import { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { Link } from 'react-router-dom'; import { Calendar, RefreshCw, Filter, ChevronRight, TrendingUp, TrendingDown, Minus, History, Search, X, Play, Loader2, Square, AlertCircle, Terminal } from 'lucide-react'; import TopPicks, { StocksToAvoid } from '../components/TopPicks'; import { DecisionBadge, HoldDaysBadge, RankBadge } from '../components/StockCard'; import TerminalModal from '../components/TerminalModal'; import HowItWorks from '../components/HowItWorks'; import BackgroundSparkline from '../components/BackgroundSparkline'; import { getLatestRecommendation, getBacktestResult as getStaticBacktestResult } from '../data/recommendations'; import { api } from '../services/api'; import { useSettings } from '../contexts/SettingsContext'; import { useNotification } from '../contexts/NotificationContext'; import { NIFTY_50_STOCKS } from '../types'; import type { Decision, StockAnalysis, DailyRecommendation, NiftyStock } from '../types'; type FilterType = 'ALL' | Decision; export default function Dashboard() { // State for real API data const [recommendation, setRecommendation] = useState(null); const [isLoadingData, setIsLoadingData] = useState(true); const [isUsingMockData, setIsUsingMockData] = useState(false); // Fetch real recommendation from API const fetchRecommendation = useCallback(async () => { setIsLoadingData(true); try { const data = await api.getLatestRecommendation(); if (data && data.analysis && Object.keys(data.analysis).length > 0) { setRecommendation(data); setIsUsingMockData(false); } else { // API returned empty data, fall back to mock const mockData = getLatestRecommendation(); setRecommendation(mockData || null); setIsUsingMockData(true); } } catch (error) { console.error('Failed to fetch recommendation from API:', error); // Fall back to mock data const mockData = getLatestRecommendation(); setRecommendation(mockData || null); setIsUsingMockData(true); } finally { setIsLoadingData(false); } }, []); // Fetch on mount useEffect(() => { fetchRecommendation(); }, [fetchRecommendation]); const [filter, setFilter] = useState('ALL'); const [searchQuery, setSearchQuery] = useState(''); const [sortBy, setSortBy] = useState<'rank' | 'symbol'>('rank'); const { settings } = useSettings(); const { addNotification } = useNotification(); // Terminal modal state const [isTerminalOpen, setIsTerminalOpen] = useState(false); // Track completed count to trigger incremental re-fetch const prevCompletedRef = useRef(0); // Bulk analysis state — initialize from localStorage for instant display on refresh const [isAnalyzing, setIsAnalyzing] = useState(() => { try { return localStorage.getItem('bulkAnalysisRunning') === 'true'; } catch { return false; } }); const [isCancelling, setIsCancelling] = useState(false); const [analysisProgress, setAnalysisProgress] = useState<{ status: string; total: number; completed: number; failed: number; skipped?: number; current_symbol: string | null; current_symbols: string[]; results: Record; parallel_workers?: number; stock_progress?: Record; } | null>(() => { try { const saved = localStorage.getItem('bulkAnalysisProgress'); return saved ? JSON.parse(saved) : null; } catch { return null; } }); // Persist analysis state to localStorage const updateAnalysisState = useCallback((analyzing: boolean, progress: typeof analysisProgress) => { setIsAnalyzing(analyzing); setAnalysisProgress(progress); try { if (analyzing) { localStorage.setItem('bulkAnalysisRunning', 'true'); if (progress) localStorage.setItem('bulkAnalysisProgress', JSON.stringify(progress)); } else { localStorage.removeItem('bulkAnalysisRunning'); localStorage.removeItem('bulkAnalysisProgress'); } } catch { /* localStorage unavailable */ } }, []); // Validate analysis state against backend on mount useEffect(() => { const checkAnalysisStatus = async () => { try { const status = await api.getBulkAnalysisStatus(); if (status.status === 'running') { updateAnalysisState(true, status); } else if (isAnalyzing) { // localStorage said running but backend says otherwise (server restarted) updateAnalysisState(false, null); } } catch (e) { console.error('Failed to check analysis status:', e); } }; checkAnalysisStatus(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Poll for analysis progress + incremental re-fetch useEffect(() => { if (!isAnalyzing) return; const pollInterval = setInterval(async () => { try { const status = await api.getBulkAnalysisStatus(); // Persist progress to localStorage on every poll updateAnalysisState(true, status); // Incremental re-fetch: when completed count increases, refresh recommendation data const newCompleted = status.completed + (status.skipped || 0); if (newCompleted > prevCompletedRef.current) { prevCompletedRef.current = newCompleted; try { const data = await api.getLatestRecommendation(); if (data && data.analysis && Object.keys(data.analysis).length > 0) { setRecommendation(data); setIsUsingMockData(false); } } catch (e) { console.warn('Re-fetch during analysis failed:', e); } } if (status.status === 'completed') { updateAnalysisState(false, null); prevCompletedRef.current = 0; clearInterval(pollInterval); // Final re-fetch for complete dataset fetchRecommendation(); addNotification({ type: 'success', title: 'Analysis Complete', message: `Successfully analyzed ${status.completed} stocks.${status.skipped ? ` ${status.skipped} already analyzed (skipped).` : ''} ${status.failed > 0 ? `${status.failed} failed.` : ''}`, duration: 8000, }); } else if (status.status === 'cancelled') { updateAnalysisState(false, null); prevCompletedRef.current = 0; setIsCancelling(false); clearInterval(pollInterval); fetchRecommendation(); addNotification({ type: 'warning', title: 'Analysis Cancelled', message: `Cancelled after analyzing ${status.completed} stocks.`, duration: 5000, }); } else if (status.status === 'idle') { updateAnalysisState(false, null); prevCompletedRef.current = 0; clearInterval(pollInterval); } } catch (e) { console.error('Failed to poll analysis status:', e); } }, 3000); return () => clearInterval(pollInterval); }, [isAnalyzing, addNotification, updateAnalysisState, fetchRecommendation]); const handleAnalyzeAll = async () => { if (isAnalyzing) return; const initialProgress = { status: 'starting', total: 50, completed: 0, failed: 0, current_symbol: null as string | null }; updateAnalysisState(true, initialProgress); try { // Pass settings from context to the API const result = await api.runBulkAnalysis(undefined, { deep_think_model: settings.deepThinkModel, quick_think_model: settings.quickThinkModel, provider: settings.provider, api_key: settings.provider === 'anthropic_api' ? settings.anthropicApiKey : undefined, max_debate_rounds: settings.maxDebateRounds, parallel_workers: settings.parallelWorkers }); // If all stocks already analyzed, exit analyzing mode if (result.status === 'completed' || result.total_stocks === 0) { updateAnalysisState(false, null); addNotification({ type: 'info', title: 'Already Analyzed', message: result.skipped ? `All ${result.skipped} stocks already analyzed for today.` : 'All stocks already analyzed for today.', duration: 5000, }); return; } addNotification({ type: 'info', title: 'Analysis Started', message: `Running AI analysis for ${result.total_stocks} stocks${result.skipped ? ` (${result.skipped} already done)` : ''}...`, duration: 3000, }); } catch (e) { console.error('Failed to start bulk analysis:', e); updateAnalysisState(false, null); addNotification({ type: 'error', title: 'Analysis Failed', message: 'Failed to start bulk analysis. Please try again.', duration: 5000, }); } }; const handleCancelAnalysis = async () => { if (!isAnalyzing || isCancelling) return; setIsCancelling(true); try { await api.cancelBulkAnalysis(); addNotification({ type: 'info', title: 'Cancelling...', message: 'Stopping analysis after current stocks complete.', duration: 3000, }); } catch (e) { console.error('Failed to cancel analysis:', e); setIsCancelling(false); addNotification({ type: 'error', title: 'Cancel Failed', message: 'Failed to cancel analysis.', duration: 3000, }); } }; // Live state for each stock in the grid type StockLiveState = 'completed' | 'analyzing' | 'pending' | 'failed'; interface StockGridItem { symbol: string; company_name: string; liveState: StockLiveState; analysis: StockAnalysis | null; } // Build unified stock grid: during analysis show all 50, otherwise only analyzed const stockGridItems = useMemo((): StockGridItem[] => { if (!isAnalyzing || !analysisProgress) { // Normal mode: only show analyzed stocks return (recommendation ? Object.values(recommendation.analysis) : []).map(s => ({ symbol: s.symbol, company_name: s.company_name, liveState: 'completed' as StockLiveState, analysis: s, })); } // Analysis mode: show all 50 stocks with live states const analysisResults = analysisProgress.results || {}; const currentSymbols = new Set(analysisProgress.current_symbols || []); const analysisMap = recommendation?.analysis || {}; return NIFTY_50_STOCKS.map((niftyStock: NiftyStock): StockGridItem => { const { symbol } = niftyStock; const resultStatus = analysisResults[symbol]; const existingAnalysis = analysisMap[symbol] || null; let liveState: StockLiveState; if (existingAnalysis) { liveState = 'completed'; } else if (resultStatus === 'completed') { liveState = 'completed'; // just completed, data not re-fetched yet } else if (resultStatus && resultStatus.startsWith('error')) { liveState = 'failed'; } else if (currentSymbols.has(symbol)) { liveState = 'analyzing'; } else { liveState = 'pending'; } return { symbol, company_name: niftyStock.company_name, liveState, analysis: existingAnalysis, }; }); }, [isAnalyzing, analysisProgress, recommendation]); // Filter grid items based on filter and search query, then sort const filteredItems = useMemo(() => { let result = stockGridItems; if (filter !== 'ALL') { // During analysis, show matching completed + all non-completed (so progress stays visible) result = result.filter(item => item.liveState !== 'completed' || item.analysis?.decision === filter ); } if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); result = result.filter(item => item.symbol.toLowerCase().includes(query) || item.company_name.toLowerCase().includes(query) ); } if (sortBy === 'rank') { result = [...result].sort((a, b) => { const aRank = a.analysis?.rank ?? Infinity; const bRank = b.analysis?.rank ?? Infinity; if (aRank !== bRank) return aRank - bRank; return a.symbol.localeCompare(b.symbol); }); } return result; }, [stockGridItems, filter, searchQuery, sortBy]); // Show loading state — but also include analysis progress banner if running if (isLoadingData || !recommendation) { return (
{isAnalyzing && analysisProgress && (
{isCancelling ? 'Cancelling...' : `Analyzing ${analysisProgress.current_symbols?.length > 0 ? analysisProgress.current_symbols.join(', ') : analysisProgress.current_symbol || 'stocks'}...`}
{analysisProgress.completed + analysisProgress.failed} / {analysisProgress.total} stocks {analysisProgress.skipped ? ` (${analysisProgress.skipped} skipped)` : ''}
0 ? ((analysisProgress.completed + analysisProgress.failed) / analysisProgress.total) * 100 : 0}%` }} />
{analysisProgress.failed > 0 && (

{analysisProgress.failed} failed

)}
)}

Loading recommendations...

Fetching data from API...

); } const { buy, sell, hold, total: analyzedTotal } = recommendation.summary; const total = isAnalyzing ? 50 : analyzedTotal; const buyPct = total > 0 ? ((buy / total) * 100).toFixed(0) : '0'; const holdPct = total > 0 ? ((hold / total) * 100).toFixed(0) : '0'; const sellPct = total > 0 ? ((sell / total) * 100).toFixed(0) : '0'; return (
{/* Compact Header with Stats */}

Nifty 50 AI Recommendations

{new Date(recommendation.date).toLocaleDateString('en-IN', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', })}
{/* Analyze All Button + Inline Stats */}
{/* Analyze All Button */} {/* Terminal Button - View Live Logs */}
setFilter('BUY')} title="Click to filter Buy stocks">
setFilter('HOLD')} title="Click to filter Hold stocks">
setFilter('SELL')} title="Click to filter Sell stocks">
{/* Progress bar */}
{/* Mock Data Indicator */} {isUsingMockData && (
Using demo data. Run "Analyze All" or start the backend server for real AI recommendations.
)} {/* Analysis Progress Banner */} {isAnalyzing && analysisProgress && (
{isCancelling ? 'Cancelling...' : ( <> Analyzing{' '} {analysisProgress.current_symbols?.length > 0 ? analysisProgress.current_symbols.join(', ') : analysisProgress.current_symbol || 'stocks'} ... )}
{analysisProgress.completed + analysisProgress.failed} / {analysisProgress.total} stocks {analysisProgress.skipped ? ` (${analysisProgress.skipped} skipped)` : ''}
0 ? ((analysisProgress.completed + analysisProgress.failed) / analysisProgress.total) * 100 : 0}%` }} />
{analysisProgress.failed > 0 && (

{analysisProgress.failed} failed

)}
)}
{/* How It Works Section */} {/* Top Picks and Avoid Section - Side by Side Compact */}
{/* All Stocks Section with Integrated Filter */}

{isAnalyzing ? `All 50 Stocks (${analysisProgress?.completed || 0} analyzed)` : `All ${total} Stocks`}

|
{/* Search Bar */}
setSearchQuery(e.target.value)} className="w-full pl-9 pr-9 py-2 text-sm rounded-lg border border-gray-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:border-transparent" /> {searchQuery && ( )}
{filteredItems.map((item) => { // COMPLETED with analysis data: clickable link if (item.liveState === 'completed' && item.analysis) { const backtest = isUsingMockData ? getStaticBacktestResult(item.symbol) : null; const trend = item.analysis.decision === 'BUY' ? 'up' : item.analysis.decision === 'SELL' ? 'down' : 'flat'; return ( {backtest && (
)}
{item.symbol}

{item.company_name}

{item.analysis.hold_days != null && item.analysis.hold_days > 0 && item.analysis.decision !== 'SELL' && (
)}
); } // COMPLETED but data not re-fetched yet (brief transition) if (item.liveState === 'completed' && !item.analysis) { return (
{item.symbol} Done

{item.company_name}

); } // ANALYZING: shimmer effect with step progress if (item.liveState === 'analyzing') { const progress = analysisProgress?.stock_progress?.[item.symbol]; const stepsDone = progress?.done ?? 0; const stepsTotal = progress?.total ?? 12; return (
{item.symbol} Analyzing

{item.company_name}

{/* Step progress bar */}
0 ? (stepsDone / stepsTotal) * 100 : 0}%` }} />
{stepsDone}/{stepsTotal}
); } // FAILED: error state if (item.liveState === 'failed') { return (
{item.symbol} Failed

{item.company_name}

); } // PENDING: grayed out, waiting return (
{item.symbol} Queued

{item.company_name}

); })}
{filteredItems.length === 0 && (

No stocks match the selected filter.

)}
{/* Compact CTA */}
); }