diff --git a/frontend/backend/database.py b/frontend/backend/database.py index 1353800c..325e1678 100644 --- a/frontend/backend/database.py +++ b/frontend/backend/database.py @@ -59,6 +59,85 @@ def init_db(): CREATE INDEX IF NOT EXISTS idx_stock_analysis_symbol ON stock_analysis(symbol) """) + # Create agent_reports table (stores each analyst's detailed report) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS agent_reports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + symbol TEXT NOT NULL, + agent_type TEXT NOT NULL, + report_content TEXT, + data_sources_used TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(date, symbol, agent_type) + ) + """) + + # Create debate_history table (stores investment and risk debates) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS debate_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + symbol TEXT NOT NULL, + debate_type TEXT NOT NULL, + bull_arguments TEXT, + bear_arguments TEXT, + risky_arguments TEXT, + safe_arguments TEXT, + neutral_arguments TEXT, + judge_decision TEXT, + full_history TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(date, symbol, debate_type) + ) + """) + + # Create pipeline_steps table (stores step-by-step execution log) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS pipeline_steps ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + symbol TEXT NOT NULL, + step_number INTEGER, + step_name TEXT, + status TEXT, + started_at TEXT, + completed_at TEXT, + duration_ms INTEGER, + output_summary TEXT, + UNIQUE(date, symbol, step_number) + ) + """) + + # Create data_source_logs table (stores what raw data was fetched) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS data_source_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + symbol TEXT NOT NULL, + source_type TEXT, + source_name TEXT, + data_fetched TEXT, + fetch_timestamp TEXT, + success INTEGER DEFAULT 1, + error_message TEXT + ) + """) + + # Create indexes for new tables + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_agent_reports_date_symbol ON agent_reports(date, symbol) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_debate_history_date_symbol ON debate_history(date, symbol) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_pipeline_steps_date_symbol ON pipeline_steps(date, symbol) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_data_source_logs_date_symbol ON data_source_logs(date, symbol) + """) + conn.commit() conn.close() @@ -219,5 +298,393 @@ def get_all_recommendations() -> list: return [get_recommendation_by_date(date) for date in dates] +# ============== Pipeline Data Functions ============== + +def save_agent_report(date: str, symbol: str, agent_type: str, + report_content: str, data_sources_used: list = None): + """Save an individual agent's report.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + INSERT OR REPLACE INTO agent_reports + (date, symbol, agent_type, report_content, data_sources_used) + VALUES (?, ?, ?, ?, ?) + """, ( + date, symbol, agent_type, report_content, + json.dumps(data_sources_used) if data_sources_used else '[]' + )) + conn.commit() + finally: + conn.close() + + +def save_agent_reports_bulk(date: str, symbol: str, reports: dict): + """Save all agent reports for a stock at once. + + Args: + date: Date string (YYYY-MM-DD) + symbol: Stock symbol + reports: Dict with keys 'market', 'news', 'social_media', 'fundamentals' + """ + conn = get_connection() + cursor = conn.cursor() + + try: + for agent_type, report_data in reports.items(): + if isinstance(report_data, str): + report_content = report_data + data_sources = [] + else: + report_content = report_data.get('content', '') + data_sources = report_data.get('data_sources', []) + + cursor.execute(""" + INSERT OR REPLACE INTO agent_reports + (date, symbol, agent_type, report_content, data_sources_used) + VALUES (?, ?, ?, ?, ?) + """, (date, symbol, agent_type, report_content, json.dumps(data_sources))) + + conn.commit() + finally: + conn.close() + + +def get_agent_reports(date: str, symbol: str) -> dict: + """Get all agent reports for a stock on a date.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + SELECT agent_type, report_content, data_sources_used, created_at + FROM agent_reports + WHERE date = ? AND symbol = ? + """, (date, symbol)) + + reports = {} + for row in cursor.fetchall(): + reports[row['agent_type']] = { + 'agent_type': row['agent_type'], + 'report_content': row['report_content'], + 'data_sources_used': json.loads(row['data_sources_used']) if row['data_sources_used'] else [], + 'created_at': row['created_at'] + } + return reports + finally: + conn.close() + + +def save_debate_history(date: str, symbol: str, debate_type: str, + bull_arguments: str = None, bear_arguments: str = None, + risky_arguments: str = None, safe_arguments: str = None, + neutral_arguments: str = None, judge_decision: str = None, + full_history: str = None): + """Save debate history for investment or risk debate.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + INSERT OR REPLACE INTO debate_history + (date, symbol, debate_type, bull_arguments, bear_arguments, + risky_arguments, safe_arguments, neutral_arguments, + judge_decision, full_history) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + date, symbol, debate_type, + bull_arguments, bear_arguments, + risky_arguments, safe_arguments, neutral_arguments, + judge_decision, full_history + )) + conn.commit() + finally: + conn.close() + + +def get_debate_history(date: str, symbol: str) -> dict: + """Get all debate history for a stock on a date.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + SELECT * FROM debate_history + WHERE date = ? AND symbol = ? + """, (date, symbol)) + + debates = {} + for row in cursor.fetchall(): + debates[row['debate_type']] = { + 'debate_type': row['debate_type'], + 'bull_arguments': row['bull_arguments'], + 'bear_arguments': row['bear_arguments'], + 'risky_arguments': row['risky_arguments'], + 'safe_arguments': row['safe_arguments'], + 'neutral_arguments': row['neutral_arguments'], + 'judge_decision': row['judge_decision'], + 'full_history': row['full_history'], + 'created_at': row['created_at'] + } + return debates + finally: + conn.close() + + +def save_pipeline_step(date: str, symbol: str, step_number: int, step_name: str, + status: str, started_at: str = None, completed_at: str = None, + duration_ms: int = None, output_summary: str = None): + """Save a pipeline step status.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + INSERT OR REPLACE INTO pipeline_steps + (date, symbol, step_number, step_name, status, + started_at, completed_at, duration_ms, output_summary) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + date, symbol, step_number, step_name, status, + started_at, completed_at, duration_ms, output_summary + )) + conn.commit() + finally: + conn.close() + + +def save_pipeline_steps_bulk(date: str, symbol: str, steps: list): + """Save all pipeline steps at once. + + Args: + date: Date string + symbol: Stock symbol + steps: List of step dicts with step_number, step_name, status, etc. + """ + conn = get_connection() + cursor = conn.cursor() + + try: + for step in steps: + cursor.execute(""" + INSERT OR REPLACE INTO pipeline_steps + (date, symbol, step_number, step_name, status, + started_at, completed_at, duration_ms, output_summary) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + date, symbol, + step.get('step_number'), + step.get('step_name'), + step.get('status'), + step.get('started_at'), + step.get('completed_at'), + step.get('duration_ms'), + step.get('output_summary') + )) + conn.commit() + finally: + conn.close() + + +def get_pipeline_steps(date: str, symbol: str) -> list: + """Get all pipeline steps for a stock on a date.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + SELECT * FROM pipeline_steps + WHERE date = ? AND symbol = ? + ORDER BY step_number + """, (date, symbol)) + + return [ + { + 'step_number': row['step_number'], + 'step_name': row['step_name'], + 'status': row['status'], + 'started_at': row['started_at'], + 'completed_at': row['completed_at'], + 'duration_ms': row['duration_ms'], + 'output_summary': row['output_summary'] + } + for row in cursor.fetchall() + ] + finally: + conn.close() + + +def save_data_source_log(date: str, symbol: str, source_type: str, + source_name: str, data_fetched: dict = None, + fetch_timestamp: str = None, success: bool = True, + error_message: str = None): + """Log a data source fetch.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + INSERT INTO data_source_logs + (date, symbol, source_type, source_name, data_fetched, + fetch_timestamp, success, error_message) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, ( + date, symbol, source_type, source_name, + json.dumps(data_fetched) if data_fetched else None, + fetch_timestamp or datetime.now().isoformat(), + 1 if success else 0, + error_message + )) + conn.commit() + finally: + conn.close() + + +def save_data_source_logs_bulk(date: str, symbol: str, logs: list): + """Save multiple data source logs at once.""" + conn = get_connection() + cursor = conn.cursor() + + try: + for log in logs: + cursor.execute(""" + INSERT INTO data_source_logs + (date, symbol, source_type, source_name, data_fetched, + fetch_timestamp, success, error_message) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, ( + date, symbol, + log.get('source_type'), + log.get('source_name'), + json.dumps(log.get('data_fetched')) if log.get('data_fetched') else None, + log.get('fetch_timestamp') or datetime.now().isoformat(), + 1 if log.get('success', True) else 0, + log.get('error_message') + )) + conn.commit() + finally: + conn.close() + + +def get_data_source_logs(date: str, symbol: str) -> list: + """Get all data source logs for a stock on a date.""" + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + SELECT * FROM data_source_logs + WHERE date = ? AND symbol = ? + ORDER BY fetch_timestamp + """, (date, symbol)) + + return [ + { + 'source_type': row['source_type'], + 'source_name': row['source_name'], + 'data_fetched': json.loads(row['data_fetched']) if row['data_fetched'] else None, + 'fetch_timestamp': row['fetch_timestamp'], + 'success': bool(row['success']), + 'error_message': row['error_message'] + } + for row in cursor.fetchall() + ] + finally: + conn.close() + + +def get_full_pipeline_data(date: str, symbol: str) -> dict: + """Get complete pipeline data for a stock on a date.""" + return { + 'date': date, + 'symbol': symbol, + 'agent_reports': get_agent_reports(date, symbol), + 'debates': get_debate_history(date, symbol), + 'pipeline_steps': get_pipeline_steps(date, symbol), + 'data_sources': get_data_source_logs(date, symbol) + } + + +def save_full_pipeline_data(date: str, symbol: str, pipeline_data: dict): + """Save complete pipeline data for a stock. + + Args: + date: Date string + symbol: Stock symbol + pipeline_data: Dict containing agent_reports, debates, pipeline_steps, data_sources + """ + if 'agent_reports' in pipeline_data: + save_agent_reports_bulk(date, symbol, pipeline_data['agent_reports']) + + if 'investment_debate' in pipeline_data: + debate = pipeline_data['investment_debate'] + save_debate_history( + date, symbol, 'investment', + bull_arguments=debate.get('bull_history'), + bear_arguments=debate.get('bear_history'), + judge_decision=debate.get('judge_decision'), + full_history=debate.get('history') + ) + + if 'risk_debate' in pipeline_data: + debate = pipeline_data['risk_debate'] + save_debate_history( + date, symbol, 'risk', + risky_arguments=debate.get('risky_history'), + safe_arguments=debate.get('safe_history'), + neutral_arguments=debate.get('neutral_history'), + judge_decision=debate.get('judge_decision'), + full_history=debate.get('history') + ) + + if 'pipeline_steps' in pipeline_data: + save_pipeline_steps_bulk(date, symbol, pipeline_data['pipeline_steps']) + + if 'data_sources' in pipeline_data: + save_data_source_logs_bulk(date, symbol, pipeline_data['data_sources']) + + +def get_pipeline_summary_for_date(date: str) -> list: + """Get pipeline summary for all stocks on a date.""" + conn = get_connection() + cursor = conn.cursor() + + try: + # Get all symbols for this date + cursor.execute(""" + SELECT DISTINCT symbol FROM stock_analysis WHERE date = ? + """, (date,)) + symbols = [row['symbol'] for row in cursor.fetchall()] + + summaries = [] + for symbol in symbols: + # Get pipeline status + cursor.execute(""" + SELECT step_name, status FROM pipeline_steps + WHERE date = ? AND symbol = ? + ORDER BY step_number + """, (date, symbol)) + steps = cursor.fetchall() + + # Get agent report count + cursor.execute(""" + SELECT COUNT(*) as count FROM agent_reports + WHERE date = ? AND symbol = ? + """, (date, symbol)) + agent_count = cursor.fetchone()['count'] + + summaries.append({ + 'symbol': symbol, + 'pipeline_steps': [{'step_name': s['step_name'], 'status': s['status']} for s in steps], + 'agent_reports_count': agent_count, + 'has_debates': bool(get_debate_history(date, symbol)) + }) + + return summaries + finally: + conn.close() + + # Initialize database on module import init_db() diff --git a/frontend/backend/recommendations.db b/frontend/backend/recommendations.db index dd4d58bb..31fc1aad 100644 Binary files a/frontend/backend/recommendations.db and b/frontend/backend/recommendations.db differ diff --git a/frontend/backend/server.py b/frontend/backend/server.py index 4a7870ee..14a8fcd6 100644 --- a/frontend/backend/server.py +++ b/frontend/backend/server.py @@ -1,9 +1,21 @@ """FastAPI server for Nifty50 AI recommendations.""" -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import Optional import database as db +import sys +import os +from pathlib import Path +from datetime import datetime +import threading + +# Add parent directories to path for importing trading agents +PROJECT_ROOT = Path(__file__).parent.parent.parent +sys.path.insert(0, str(PROJECT_ROOT)) + +# Track running analyses +running_analyses = {} # {symbol: {"status": "running", "started_at": datetime, "progress": str}} app = FastAPI( title="Nifty50 AI API", @@ -68,19 +80,131 @@ class SaveRecommendationRequest(BaseModel): stocks_to_avoid: list +# ============== Pipeline Data Models ============== + +class AgentReport(BaseModel): + agent_type: str + report_content: str + data_sources_used: Optional[list] = [] + created_at: Optional[str] = None + + +class DebateHistory(BaseModel): + debate_type: str + bull_arguments: Optional[str] = None + bear_arguments: Optional[str] = None + risky_arguments: Optional[str] = None + safe_arguments: Optional[str] = None + neutral_arguments: Optional[str] = None + judge_decision: Optional[str] = None + full_history: Optional[str] = None + + +class PipelineStep(BaseModel): + step_number: int + step_name: str + status: str + started_at: Optional[str] = None + completed_at: Optional[str] = None + duration_ms: Optional[int] = None + output_summary: Optional[str] = None + + +class DataSourceLog(BaseModel): + source_type: str + source_name: str + data_fetched: Optional[dict] = None + fetch_timestamp: Optional[str] = None + success: bool = True + error_message: Optional[str] = None + + +class SavePipelineDataRequest(BaseModel): + date: str + symbol: str + agent_reports: Optional[dict] = None + investment_debate: Optional[dict] = None + risk_debate: Optional[dict] = None + pipeline_steps: Optional[list] = None + data_sources: Optional[list] = None + + +class RunAnalysisRequest(BaseModel): + symbol: str + date: Optional[str] = None # Defaults to today if not provided + + +def run_analysis_task(symbol: str, date: str): + """Background task to run trading analysis for a stock.""" + global running_analyses + + try: + running_analyses[symbol] = { + "status": "initializing", + "started_at": datetime.now().isoformat(), + "progress": "Loading trading agents..." + } + + # Import trading agents + from tradingagents.graph.trading_graph import TradingAgentsGraph + from tradingagents.default_config import DEFAULT_CONFIG + + running_analyses[symbol]["progress"] = "Initializing analysis pipeline..." + + # Create config + config = DEFAULT_CONFIG.copy() + config["llm_provider"] = "anthropic" # Use Claude for all LLM + config["deep_think_llm"] = "opus" # Claude Opus (Claude Max CLI alias) + config["quick_think_llm"] = "sonnet" # Claude Sonnet (Claude Max CLI alias) + config["max_debate_rounds"] = 1 + + running_analyses[symbol]["status"] = "running" + running_analyses[symbol]["progress"] = "Running market analysis..." + + # Initialize and run + ta = TradingAgentsGraph(debug=False, config=config) + + running_analyses[symbol]["progress"] = f"Analyzing {symbol}..." + final_state, decision = ta.propagate(symbol, date) + + running_analyses[symbol] = { + "status": "completed", + "completed_at": datetime.now().isoformat(), + "progress": f"Analysis complete: {decision}", + "decision": decision + } + + except Exception as e: + error_msg = str(e) if str(e) else f"{type(e).__name__}: No details provided" + running_analyses[symbol] = { + "status": "error", + "error": error_msg, + "progress": f"Error: {error_msg[:100]}" + } + import traceback + print(f"Analysis error for {symbol}: {type(e).__name__}: {error_msg}") + traceback.print_exc() + + @app.get("/") async def root(): """API root endpoint.""" return { "name": "Nifty50 AI API", - "version": "1.0.0", + "version": "2.0.0", "endpoints": { "GET /recommendations": "Get all recommendations", "GET /recommendations/latest": "Get latest recommendation", "GET /recommendations/{date}": "Get recommendation by date", + "GET /recommendations/{date}/{symbol}/pipeline": "Get full pipeline data for a stock", + "GET /recommendations/{date}/{symbol}/agents": "Get agent reports for a stock", + "GET /recommendations/{date}/{symbol}/debates": "Get debate history for a stock", + "GET /recommendations/{date}/{symbol}/data-sources": "Get data source logs for a stock", + "GET /recommendations/{date}/pipeline-summary": "Get pipeline summary for all stocks on a date", "GET /stocks/{symbol}/history": "Get stock history", "GET /dates": "Get all available dates", - "POST /recommendations": "Save a new recommendation" + "POST /recommendations": "Save a new recommendation", + "POST /pipeline": "Save pipeline data for a stock" } } @@ -146,6 +270,160 @@ async def health_check(): return {"status": "healthy", "database": "connected"} +# ============== Pipeline Data Endpoints ============== + +@app.get("/recommendations/{date}/{symbol}/pipeline") +async def get_pipeline_data(date: str, symbol: str): + """Get full pipeline data for a stock on a specific date.""" + pipeline_data = db.get_full_pipeline_data(date, symbol.upper()) + + # Check if we have any data + has_data = ( + pipeline_data.get('agent_reports') or + pipeline_data.get('debates') or + pipeline_data.get('pipeline_steps') or + pipeline_data.get('data_sources') + ) + + if not has_data: + # Return empty structure with mock pipeline steps if no data + return { + "date": date, + "symbol": symbol.upper(), + "agent_reports": {}, + "debates": {}, + "pipeline_steps": [], + "data_sources": [], + "status": "no_data" + } + + return {**pipeline_data, "status": "complete"} + + +@app.get("/recommendations/{date}/{symbol}/agents") +async def get_agent_reports(date: str, symbol: str): + """Get agent reports for a stock on a specific date.""" + reports = db.get_agent_reports(date, symbol.upper()) + return { + "date": date, + "symbol": symbol.upper(), + "reports": reports, + "count": len(reports) + } + + +@app.get("/recommendations/{date}/{symbol}/debates") +async def get_debate_history(date: str, symbol: str): + """Get debate history for a stock on a specific date.""" + debates = db.get_debate_history(date, symbol.upper()) + return { + "date": date, + "symbol": symbol.upper(), + "debates": debates + } + + +@app.get("/recommendations/{date}/{symbol}/data-sources") +async def get_data_sources(date: str, symbol: str): + """Get data source logs for a stock on a specific date.""" + logs = db.get_data_source_logs(date, symbol.upper()) + return { + "date": date, + "symbol": symbol.upper(), + "data_sources": logs, + "count": len(logs) + } + + +@app.get("/recommendations/{date}/pipeline-summary") +async def get_pipeline_summary(date: str): + """Get pipeline summary for all stocks on a specific date.""" + summary = db.get_pipeline_summary_for_date(date) + return { + "date": date, + "stocks": summary, + "count": len(summary) + } + + +@app.post("/pipeline") +async def save_pipeline_data(request: SavePipelineDataRequest): + """Save pipeline data for a stock.""" + try: + db.save_full_pipeline_data( + date=request.date, + symbol=request.symbol.upper(), + pipeline_data={ + 'agent_reports': request.agent_reports, + 'investment_debate': request.investment_debate, + 'risk_debate': request.risk_debate, + 'pipeline_steps': request.pipeline_steps, + 'data_sources': request.data_sources + } + ) + return {"message": f"Pipeline data for {request.symbol} on {request.date} saved successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# ============== Analysis Endpoints ============== + +@app.post("/analyze/{symbol}") +async def run_analysis(symbol: str, background_tasks: BackgroundTasks, date: Optional[str] = None): + """Trigger analysis for a stock. Runs in background.""" + symbol = symbol.upper() + + # Check if analysis is already running + if symbol in running_analyses and running_analyses[symbol].get("status") == "running": + return { + "message": f"Analysis already running for {symbol}", + "status": running_analyses[symbol] + } + + # Use today's date if not provided + if not date: + date = datetime.now().strftime("%Y-%m-%d") + + # Start analysis in background thread + thread = threading.Thread(target=run_analysis_task, args=(symbol, date)) + thread.start() + + return { + "message": f"Analysis started for {symbol}", + "symbol": symbol, + "date": date, + "status": "started" + } + + +@app.get("/analyze/{symbol}/status") +async def get_analysis_status(symbol: str): + """Get the status of a running or completed analysis.""" + symbol = symbol.upper() + + if symbol not in running_analyses: + return { + "symbol": symbol, + "status": "not_started", + "message": "No analysis has been run for this stock" + } + + return { + "symbol": symbol, + **running_analyses[symbol] + } + + +@app.get("/analyze/running") +async def get_running_analyses(): + """Get all currently running analyses.""" + running = {k: v for k, v in running_analyses.items() if v.get("status") == "running"} + return { + "running": running, + "count": len(running) + } + + if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) + uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/frontend/package.json b/frontend/package.json index 498c5815..360f155c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "playwright": "^1.58.1", "postcss": "^8.5.6", "puppeteer": "^24.36.1", "tailwindcss": "^4.1.18", diff --git a/frontend/src/components/pipeline/AgentReportCard.tsx b/frontend/src/components/pipeline/AgentReportCard.tsx new file mode 100644 index 00000000..4b5610b0 --- /dev/null +++ b/frontend/src/components/pipeline/AgentReportCard.tsx @@ -0,0 +1,194 @@ +import { useState } from 'react'; +import { + TrendingUp, Newspaper, Users, FileText, + ChevronDown, ChevronUp, Database, Clock, CheckCircle +} from 'lucide-react'; +import type { AgentReport, AgentType } from '../../types/pipeline'; +import { AGENT_METADATA } from '../../types/pipeline'; + +interface AgentReportCardProps { + agentType: AgentType; + report?: AgentReport; + isLoading?: boolean; +} + +const AGENT_ICONS: Record = { + market: TrendingUp, + news: Newspaper, + social_media: Users, + fundamentals: FileText, +}; + +const AGENT_COLORS: Record = { + market: { + bg: 'bg-blue-50 dark:bg-blue-900/20', + border: 'border-blue-200 dark:border-blue-800', + text: 'text-blue-700 dark:text-blue-300', + accent: 'bg-blue-500' + }, + news: { + bg: 'bg-purple-50 dark:bg-purple-900/20', + border: 'border-purple-200 dark:border-purple-800', + text: 'text-purple-700 dark:text-purple-300', + accent: 'bg-purple-500' + }, + social_media: { + bg: 'bg-pink-50 dark:bg-pink-900/20', + border: 'border-pink-200 dark:border-pink-800', + text: 'text-pink-700 dark:text-pink-300', + accent: 'bg-pink-500' + }, + fundamentals: { + bg: 'bg-green-50 dark:bg-green-900/20', + border: 'border-green-200 dark:border-green-800', + text: 'text-green-700 dark:text-green-300', + accent: 'bg-green-500' + }, +}; + +export function AgentReportCard({ agentType, report, isLoading }: AgentReportCardProps) { + const [isExpanded, setIsExpanded] = useState(false); + + const Icon = AGENT_ICONS[agentType]; + const colors = AGENT_COLORS[agentType]; + const metadata = AGENT_METADATA[agentType]; + const hasReport = report && report.report_content; + + // Parse markdown-like content into sections + const parseContent = (content: string) => { + const lines = content.split('\n'); + const sections: { title: string; content: string[] }[] = []; + let currentSection: { title: string; content: string[] } | null = null; + + lines.forEach(line => { + if (line.startsWith('##') || line.startsWith('**')) { + if (currentSection) { + sections.push(currentSection); + } + currentSection = { + title: line.replace(/^#+\s*/, '').replace(/\*\*/g, ''), + content: [] + }; + } else if (currentSection && line.trim()) { + currentSection.content.push(line); + } + }); + + if (currentSection) { + sections.push(currentSection); + } + + return sections; + }; + + const sections = hasReport ? parseContent(report.report_content) : []; + const previewText = hasReport + ? report.report_content.slice(0, 200).replace(/[#*]/g, '') + '...' + : 'No analysis available'; + + return ( +
+ {/* Header */} +
hasReport && setIsExpanded(!isExpanded)} + > +
+
+ +
+
+

{metadata.label}

+

+ {metadata.description} +

+
+
+ +
+ {hasReport ? ( + + ) : isLoading ? ( +
+ ) : ( + + )} + + {hasReport && ( + isExpanded ? ( + + ) : ( + + ) + )} +
+
+ + {/* Preview (collapsed) */} + {!isExpanded && hasReport && ( +
+

+ {previewText} +

+
+ )} + + {/* Expanded content */} + {isExpanded && hasReport && ( +
+ {/* Data sources */} + {report.data_sources_used && report.data_sources_used.length > 0 && ( +
+ + Sources: + {report.data_sources_used.map((source, idx) => ( + + {source} + + ))} +
+ )} + + {/* Report content */} +
+ {sections.length > 0 ? ( + sections.map((section, idx) => ( +
+

+ {section.title} +

+
+ {section.content.map((line, lineIdx) => ( +

{line}

+ ))} +
+
+ )) + ) : ( +
+
+                  {report.report_content}
+                
+
+ )} +
+ + {/* Timestamp */} + {report.created_at && ( +
+ + + Generated: {new Date(report.created_at).toLocaleString()} + +
+ )} +
+ )} +
+ ); +} + +export default AgentReportCard; diff --git a/frontend/src/components/pipeline/DataSourcesPanel.tsx b/frontend/src/components/pipeline/DataSourcesPanel.tsx new file mode 100644 index 00000000..1c4f93f1 --- /dev/null +++ b/frontend/src/components/pipeline/DataSourcesPanel.tsx @@ -0,0 +1,185 @@ +import { useState } from 'react'; +import { + Database, ChevronDown, ChevronUp, CheckCircle, + XCircle, Clock, ExternalLink, Server +} from 'lucide-react'; +import type { DataSourceLog } from '../../types/pipeline'; + +interface DataSourcesPanelProps { + dataSources: DataSourceLog[]; + isLoading?: boolean; +} + +const SOURCE_TYPE_COLORS: Record = { + market_data: { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300' }, + news: { bg: 'bg-purple-100 dark:bg-purple-900/30', text: 'text-purple-700 dark:text-purple-300' }, + fundamentals: { bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-700 dark:text-green-300' }, + social_media: { bg: 'bg-pink-100 dark:bg-pink-900/30', text: 'text-pink-700 dark:text-pink-300' }, + indicators: { bg: 'bg-amber-100 dark:bg-amber-900/30', text: 'text-amber-700 dark:text-amber-300' }, + default: { bg: 'bg-slate-100 dark:bg-slate-800', text: 'text-slate-700 dark:text-slate-300' } +}; + +export function DataSourcesPanel({ dataSources, isLoading }: DataSourcesPanelProps) { + const [isExpanded, setIsExpanded] = useState(false); + const [expandedSources, setExpandedSources] = useState>(new Set()); + + const hasData = dataSources.length > 0; + const successCount = dataSources.filter(s => s.success).length; + const errorCount = dataSources.filter(s => !s.success).length; + + const toggleSourceExpanded = (index: number) => { + const newSet = new Set(expandedSources); + if (newSet.has(index)) { + newSet.delete(index); + } else { + newSet.add(index); + } + setExpandedSources(newSet); + }; + + const getSourceColors = (sourceType: string) => { + return SOURCE_TYPE_COLORS[sourceType] || SOURCE_TYPE_COLORS.default; + }; + + const formatTimestamp = (timestamp?: string) => { + if (!timestamp) return 'Unknown'; + try { + return new Date(timestamp).toLocaleString(); + } catch { + return timestamp; + } + }; + + return ( +
+ {/* Header */} +
hasData && setIsExpanded(!isExpanded)} + > +
+
+ +
+
+

+ Data Sources +

+

+ Raw data fetched for analysis +

+
+
+ +
+ {hasData ? ( +
+ + + {successCount} + + {errorCount > 0 && ( + + + {errorCount} + + )} +
+ ) : isLoading ? ( +
+ ) : ( + + No Data + + )} + {hasData && ( + isExpanded ? : + )} +
+
+ + {/* Expanded content */} + {isExpanded && hasData && ( +
+
+ {dataSources.map((source, index) => { + const colors = getSourceColors(source.source_type); + const isSourceExpanded = expandedSources.has(index); + + return ( +
+ {/* Source header */} +
toggleSourceExpanded(index)} + > +
+ +
+
+ + {source.source_type} + + + {source.source_name} + +
+
+ + {formatTimestamp(source.fetch_timestamp)} +
+
+
+ +
+ {source.success ? ( + + ) : ( + + )} + {isSourceExpanded ? ( + + ) : ( + + )} +
+
+ + {/* Source details (expanded) */} + {isSourceExpanded && ( +
+ {source.error_message ? ( +
+

+ Error: {source.error_message} +

+
+ ) : source.data_fetched ? ( +
+

+ Data Summary: +

+
+                            {typeof source.data_fetched === 'string'
+                              ? source.data_fetched.slice(0, 500) + (source.data_fetched.length > 500 ? '...' : '')
+                              : JSON.stringify(source.data_fetched, null, 2).slice(0, 500)}
+                          
+
+ ) : ( +

+ No data details available +

+ )} +
+ )} +
+ ); + })} +
+
+ )} +
+ ); +} + +export default DataSourcesPanel; diff --git a/frontend/src/components/pipeline/DebateViewer.tsx b/frontend/src/components/pipeline/DebateViewer.tsx new file mode 100644 index 00000000..b9f9f385 --- /dev/null +++ b/frontend/src/components/pipeline/DebateViewer.tsx @@ -0,0 +1,254 @@ +import { useState } from 'react'; +import { + TrendingUp, TrendingDown, Scale, ChevronDown, ChevronUp, + MessageSquare, Award, Clock +} from 'lucide-react'; +import type { DebateHistory } from '../../types/pipeline'; + +interface DebateViewerProps { + debate?: DebateHistory; + isLoading?: boolean; +} + +export function DebateViewer({ debate, isLoading }: DebateViewerProps) { + const [isExpanded, setIsExpanded] = useState(false); + const [activeTab, setActiveTab] = useState<'bull' | 'bear' | 'history'>('history'); + + const hasDebate = debate && (debate.bull_arguments || debate.bear_arguments || debate.full_history); + + // Parse debate rounds from full history + const parseDebateRounds = (history: string) => { + const rounds: { speaker: string; content: string }[] = []; + const lines = history.split('\n'); + + let currentSpeaker = ''; + let currentContent: string[] = []; + + lines.forEach(line => { + if (line.startsWith('Bull') || line.startsWith('Bear') || line.startsWith('Judge')) { + if (currentSpeaker && currentContent.length > 0) { + rounds.push({ + speaker: currentSpeaker, + content: currentContent.join('\n') + }); + } + currentSpeaker = line.split(':')[0] || line.split(' ')[0]; + currentContent = [line.substring(line.indexOf(':') + 1).trim()]; + } else if (line.trim()) { + currentContent.push(line); + } + }); + + if (currentSpeaker && currentContent.length > 0) { + rounds.push({ + speaker: currentSpeaker, + content: currentContent.join('\n') + }); + } + + return rounds; + }; + + const debateRounds = hasDebate && debate.full_history + ? parseDebateRounds(debate.full_history) + : []; + + const getSpeakerStyle = (speaker: string) => { + if (speaker.toLowerCase().includes('bull')) { + return { + bg: 'bg-green-50 dark:bg-green-900/20', + border: 'border-l-green-500', + icon: TrendingUp, + color: 'text-green-600 dark:text-green-400' + }; + } else if (speaker.toLowerCase().includes('bear')) { + return { + bg: 'bg-red-50 dark:bg-red-900/20', + border: 'border-l-red-500', + icon: TrendingDown, + color: 'text-red-600 dark:text-red-400' + }; + } else { + return { + bg: 'bg-blue-50 dark:bg-blue-900/20', + border: 'border-l-blue-500', + icon: Scale, + color: 'text-blue-600 dark:text-blue-400' + }; + } + }; + + return ( +
+ {/* Header */} +
hasDebate && setIsExpanded(!isExpanded)} + > +
+
+
+ +
+
+ +
+
+ +
+
+
+

+ Investment Debate +

+

+ Bull vs Bear Analysis with Research Manager Decision +

+
+
+ +
+ {hasDebate ? ( + + Complete + + ) : isLoading ? ( +
+ ) : ( + + No Data + + )} + {hasDebate && ( + isExpanded ? : + )} +
+
+ + {/* Expanded content */} + {isExpanded && hasDebate && ( +
+ {/* Tabs */} +
+ + + +
+ + {/* Content */} +
+ {activeTab === 'history' && ( +
+ {debateRounds.length > 0 ? ( + debateRounds.map((round, idx) => { + const style = getSpeakerStyle(round.speaker); + const Icon = style.icon; + return ( +
+
+ + + {round.speaker} + +
+

+ {round.content} +

+
+ ); + }) + ) : debate.full_history ? ( +
+                    {debate.full_history}
+                  
+ ) : ( +

No debate history available

+ )} +
+ )} + + {activeTab === 'bull' && ( +
+
+ + + Bull Analyst Arguments + +
+

+ {debate.bull_arguments || 'No bull arguments recorded'} +

+
+ )} + + {activeTab === 'bear' && ( +
+
+ + + Bear Analyst Arguments + +
+

+ {debate.bear_arguments || 'No bear arguments recorded'} +

+
+ )} +
+ + {/* Judge Decision */} + {debate.judge_decision && ( +
+
+ +
+

+ Research Manager Decision +

+

+ {debate.judge_decision} +

+
+
+
+ )} +
+ )} +
+ ); +} + +export default DebateViewer; diff --git a/frontend/src/components/pipeline/PipelineOverview.tsx b/frontend/src/components/pipeline/PipelineOverview.tsx new file mode 100644 index 00000000..78e6157b --- /dev/null +++ b/frontend/src/components/pipeline/PipelineOverview.tsx @@ -0,0 +1,157 @@ +import { + Database, TrendingUp, Newspaper, Users, FileText, + MessageSquare, Target, Shield, CheckCircle, Loader2, + AlertCircle, Clock +} from 'lucide-react'; +import type { PipelineStep, PipelineStepStatus } from '../../types/pipeline'; + +interface PipelineOverviewProps { + steps: PipelineStep[]; + onStepClick?: (step: PipelineStep) => void; + compact?: boolean; +} + +const STEP_ICONS: Record = { + data_collection: Database, + market_analysis: TrendingUp, + news_analysis: Newspaper, + social_analysis: Users, + fundamentals_analysis: FileText, + investment_debate: MessageSquare, + trader_decision: Target, + risk_debate: Shield, + final_decision: CheckCircle, +}; + +const STEP_LABELS: Record = { + data_collection: 'Data Collection', + market_analysis: 'Market Analysis', + news_analysis: 'News Analysis', + social_analysis: 'Social Analysis', + fundamentals_analysis: 'Fundamentals', + investment_debate: 'Investment Debate', + trader_decision: 'Trader Decision', + risk_debate: 'Risk Assessment', + final_decision: 'Final Decision', +}; + +const STATUS_STYLES: Record = { + pending: { + bg: 'bg-slate-100 dark:bg-slate-800', + border: 'border-slate-300 dark:border-slate-600', + text: 'text-slate-400 dark:text-slate-500', + icon: Clock + }, + running: { + bg: 'bg-blue-50 dark:bg-blue-900/30', + border: 'border-blue-400 dark:border-blue-500', + text: 'text-blue-600 dark:text-blue-400', + icon: Loader2 + }, + completed: { + bg: 'bg-green-50 dark:bg-green-900/30', + border: 'border-green-400 dark:border-green-500', + text: 'text-green-600 dark:text-green-400', + icon: CheckCircle + }, + error: { + bg: 'bg-red-50 dark:bg-red-900/30', + border: 'border-red-400 dark:border-red-500', + text: 'text-red-600 dark:text-red-400', + icon: AlertCircle + }, +}; + +// Default pipeline steps when no data is available +const DEFAULT_STEPS: PipelineStep[] = [ + { step_number: 1, step_name: 'data_collection', status: 'pending' }, + { step_number: 2, step_name: 'market_analysis', status: 'pending' }, + { step_number: 3, step_name: 'news_analysis', status: 'pending' }, + { step_number: 4, step_name: 'social_analysis', status: 'pending' }, + { step_number: 5, step_name: 'fundamentals_analysis', status: 'pending' }, + { step_number: 6, step_name: 'investment_debate', status: 'pending' }, + { step_number: 7, step_name: 'trader_decision', status: 'pending' }, + { step_number: 8, step_name: 'risk_debate', status: 'pending' }, + { step_number: 9, step_name: 'final_decision', status: 'pending' }, +]; + +export function PipelineOverview({ steps, onStepClick, compact = false }: PipelineOverviewProps) { + const displaySteps = steps.length > 0 ? steps : DEFAULT_STEPS; + + const completedCount = displaySteps.filter(s => s.status === 'completed').length; + const totalSteps = displaySteps.length; + const progress = Math.round((completedCount / totalSteps) * 100); + + if (compact) { + return ( +
+ {displaySteps.map((step, index) => { + const styles = STATUS_STYLES[step.status]; + return ( +
+ ); + })} + {progress}% +
+ ); + } + + return ( +
+ {/* Progress bar */} +
+
+
+
+ + {completedCount}/{totalSteps} + +
+ + {/* Pipeline steps */} +
+ {displaySteps.map((step, index) => { + const StepIcon = STEP_ICONS[step.step_name] || Database; + const styles = STATUS_STYLES[step.status]; + const StatusIcon = styles.icon; + const label = STEP_LABELS[step.step_name] || step.step_name; + + return ( + + ); + })} +
+
+ ); +} + +export default PipelineOverview; diff --git a/frontend/src/components/pipeline/RiskDebateViewer.tsx b/frontend/src/components/pipeline/RiskDebateViewer.tsx new file mode 100644 index 00000000..98d32b3b --- /dev/null +++ b/frontend/src/components/pipeline/RiskDebateViewer.tsx @@ -0,0 +1,256 @@ +import { useState } from 'react'; +import { + Zap, Shield, Scale, ChevronDown, ChevronUp, + ShieldCheck, AlertTriangle +} from 'lucide-react'; +import type { DebateHistory } from '../../types/pipeline'; + +interface RiskDebateViewerProps { + debate?: DebateHistory; + isLoading?: boolean; +} + +export function RiskDebateViewer({ debate, isLoading }: RiskDebateViewerProps) { + const [isExpanded, setIsExpanded] = useState(false); + const [activeTab, setActiveTab] = useState<'all' | 'risky' | 'safe' | 'neutral'>('all'); + + const hasDebate = debate && ( + debate.risky_arguments || + debate.safe_arguments || + debate.neutral_arguments || + debate.full_history + ); + + const ROLE_STYLES = { + risky: { + bg: 'bg-red-50 dark:bg-red-900/20', + border: 'border-l-red-500', + icon: Zap, + color: 'text-red-600 dark:text-red-400', + label: 'Aggressive Analyst' + }, + safe: { + bg: 'bg-green-50 dark:bg-green-900/20', + border: 'border-l-green-500', + icon: Shield, + color: 'text-green-600 dark:text-green-400', + label: 'Conservative Analyst' + }, + neutral: { + bg: 'bg-slate-50 dark:bg-slate-800/50', + border: 'border-l-slate-500', + icon: Scale, + color: 'text-slate-600 dark:text-slate-400', + label: 'Neutral Analyst' + } + }; + + return ( +
+ {/* Header */} +
hasDebate && setIsExpanded(!isExpanded)} + > +
+
+
+ +
+
+ +
+
+ +
+
+
+

+ Risk Assessment Debate +

+

+ Aggressive vs Conservative vs Neutral with Risk Manager Decision +

+
+
+ +
+ {hasDebate ? ( + + Complete + + ) : isLoading ? ( +
+ ) : ( + + No Data + + )} + {hasDebate && ( + isExpanded ? : + )} +
+
+ + {/* Expanded content */} + {isExpanded && hasDebate && ( +
+ {/* Tabs */} +
+ + + + +
+ + {/* Content */} +
+ {activeTab === 'all' && ( +
+ {/* Aggressive */} +
+
+ + + {ROLE_STYLES.risky.label} + +
+

+ {debate.risky_arguments || 'No arguments recorded'} +

+
+ + {/* Neutral */} +
+
+ + + {ROLE_STYLES.neutral.label} + +
+

+ {debate.neutral_arguments || 'No arguments recorded'} +

+
+ + {/* Conservative */} +
+
+ + + {ROLE_STYLES.safe.label} + +
+

+ {debate.safe_arguments || 'No arguments recorded'} +

+
+
+ )} + + {activeTab === 'risky' && ( +
+
+ + + {ROLE_STYLES.risky.label} + + +
+

+ {debate.risky_arguments || 'No aggressive arguments recorded'} +

+
+ )} + + {activeTab === 'neutral' && ( +
+
+ + + {ROLE_STYLES.neutral.label} + +
+

+ {debate.neutral_arguments || 'No neutral arguments recorded'} +

+
+ )} + + {activeTab === 'safe' && ( +
+
+ + + {ROLE_STYLES.safe.label} + +
+

+ {debate.safe_arguments || 'No conservative arguments recorded'} +

+
+ )} +
+ + {/* Risk Manager Decision */} + {debate.judge_decision && ( +
+
+ +
+

+ Risk Manager Decision +

+

+ {debate.judge_decision} +

+
+
+
+ )} +
+ )} +
+ ); +} + +export default RiskDebateViewer; diff --git a/frontend/src/components/pipeline/index.ts b/frontend/src/components/pipeline/index.ts new file mode 100644 index 00000000..e33595ab --- /dev/null +++ b/frontend/src/components/pipeline/index.ts @@ -0,0 +1,5 @@ +export { PipelineOverview } from './PipelineOverview'; +export { AgentReportCard } from './AgentReportCard'; +export { DebateViewer } from './DebateViewer'; +export { RiskDebateViewer } from './RiskDebateViewer'; +export { DataSourcesPanel } from './DataSourcesPanel'; diff --git a/frontend/src/pages/StockDetail.tsx b/frontend/src/pages/StockDetail.tsx index 9385a538..2bc52e14 100644 --- a/frontend/src/pages/StockDetail.tsx +++ b/frontend/src/pages/StockDetail.tsx @@ -1,14 +1,40 @@ import { useParams, Link } from 'react-router-dom'; -import { useMemo } from 'react'; -import { ArrowLeft, Building2, TrendingUp, TrendingDown, Minus, AlertTriangle, Calendar, Activity, LineChart } from 'lucide-react'; +import { useMemo, useState, useEffect } from 'react'; +import { + ArrowLeft, Building2, TrendingUp, TrendingDown, Minus, AlertTriangle, + Calendar, Activity, LineChart, Database, MessageSquare, FileText, Layers, + RefreshCw, Play, Loader2 +} from 'lucide-react'; import { NIFTY_50_STOCKS } from '../types'; import { sampleRecommendations, getStockHistory, getExtendedPriceHistory, getPredictionPointsWithPrices, getRawAnalysis } from '../data/recommendations'; import { DecisionBadge, ConfidenceBadge, RiskBadge } from '../components/StockCard'; import AIAnalysisPanel from '../components/AIAnalysisPanel'; import StockPriceChart from '../components/StockPriceChart'; +import { + PipelineOverview, + AgentReportCard, + DebateViewer, + RiskDebateViewer, + DataSourcesPanel +} from '../components/pipeline'; +import { api } from '../services/api'; +import type { FullPipelineData, AgentType } from '../types/pipeline'; + +type TabType = 'overview' | 'pipeline' | 'debates' | 'data'; export default function StockDetail() { const { symbol } = useParams<{ symbol: string }>(); + const [activeTab, setActiveTab] = useState('overview'); + const [pipelineData, setPipelineData] = useState(null); + const [isLoadingPipeline, setIsLoadingPipeline] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(false); + const [lastRefresh, setLastRefresh] = useState(null); + const [refreshMessage, setRefreshMessage] = useState(null); + + // Analysis state + const [isAnalysisRunning, setIsAnalysisRunning] = useState(false); + const [analysisStatus, setAnalysisStatus] = useState(null); + const [analysisProgress, setAnalysisProgress] = useState(null); const stock = NIFTY_50_STOCKS.find(s => s.symbol === symbol); const latestRecommendation = sampleRecommendations[0]; @@ -26,6 +52,119 @@ export default function StockDetail() { : []; }, [symbol, priceHistory]); + // Function to fetch pipeline data + const fetchPipelineData = async (forceRefresh = false) => { + if (!symbol || !latestRecommendation?.date) return; + + if (forceRefresh) { + setIsRefreshing(true); + } else { + setIsLoadingPipeline(true); + } + + try { + const data = await api.getPipelineData(latestRecommendation.date, symbol, forceRefresh); + setPipelineData(data); + if (forceRefresh) { + setLastRefresh(new Date().toLocaleTimeString()); + const hasData = data.pipeline_steps?.length > 0 || Object.keys(data.agent_reports || {}).length > 0; + setRefreshMessage(hasData ? `✓ Data refreshed for ${symbol}` : `No pipeline data found for ${symbol}`); + setTimeout(() => setRefreshMessage(null), 3000); + } + console.log('Pipeline data fetched:', data); + } catch (error) { + console.error('Failed to fetch pipeline data:', error); + if (forceRefresh) { + setRefreshMessage(`✗ Failed to refresh: ${error}`); + setTimeout(() => setRefreshMessage(null), 3000); + } + // Set empty pipeline data structure + setPipelineData({ + date: latestRecommendation.date, + symbol: symbol, + agent_reports: {}, + debates: {}, + pipeline_steps: [], + data_sources: [], + status: 'no_data' + }); + } finally { + setIsLoadingPipeline(false); + setIsRefreshing(false); + } + }; + + // Fetch pipeline data when tab changes or symbol changes + useEffect(() => { + if (activeTab === 'overview') return; // Don't fetch for overview tab + fetchPipelineData(); + }, [symbol, latestRecommendation?.date, activeTab]); + + // Refresh handler + const handleRefresh = async () => { + console.log('Refresh button clicked - fetching fresh data...'); + await fetchPipelineData(true); + console.log('Refresh complete - data updated'); + }; + + // Run Analysis handler + const handleRunAnalysis = async () => { + if (!symbol || !latestRecommendation?.date) return; + + setIsAnalysisRunning(true); + setAnalysisStatus('starting'); + setAnalysisProgress('Starting analysis...'); + + try { + // Trigger analysis + await api.runAnalysis(symbol, latestRecommendation.date); + setAnalysisStatus('running'); + + // Poll for status + const pollInterval = setInterval(async () => { + try { + const status = await api.getAnalysisStatus(symbol); + setAnalysisProgress(status.progress || 'Processing...'); + + if (status.status === 'completed') { + clearInterval(pollInterval); + setIsAnalysisRunning(false); + setAnalysisStatus('completed'); + setAnalysisProgress(`✓ Analysis complete: ${status.decision || 'Done'}`); + // Refresh data to show results + await fetchPipelineData(true); + setTimeout(() => { + setAnalysisProgress(null); + setAnalysisStatus(null); + }, 5000); + } else if (status.status === 'error') { + clearInterval(pollInterval); + setIsAnalysisRunning(false); + setAnalysisStatus('error'); + setAnalysisProgress(`✗ Error: ${status.error}`); + } + } catch (err) { + console.error('Failed to poll analysis status:', err); + } + }, 2000); // Poll every 2 seconds + + // Cleanup after 10 minutes max + setTimeout(() => clearInterval(pollInterval), 600000); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error('Failed to start analysis:', errorMessage, error); + setIsAnalysisRunning(false); + setAnalysisStatus('error'); + // More helpful error message + if (errorMessage.includes('Failed to fetch') || errorMessage.includes('NetworkError')) { + setAnalysisProgress(`✗ Network error: Cannot connect to backend at localhost:8000. Please check if the server is running.`); + } else { + setAnalysisProgress(`✗ Failed to start analysis: ${errorMessage}`); + } + } + }; + if (!stock) { return (
@@ -56,6 +195,13 @@ export default function StockDetail() { const DecisionIcon = analysis?.decision ? decisionIcon[analysis.decision] : Activity; const bgGradient = analysis?.decision ? decisionColor[analysis.decision] : 'from-gray-500 to-gray-600'; + const TABS = [ + { id: 'overview' as const, label: 'Overview', icon: LineChart }, + { id: 'pipeline' as const, label: 'Analysis Pipeline', icon: Layers }, + { id: 'debates' as const, label: 'Debates', icon: MessageSquare }, + { id: 'data' as const, label: 'Data Sources', icon: Database }, + ]; + return (
{/* Back Button */} @@ -120,89 +266,266 @@ export default function StockDetail() { )} - {/* Price Chart with Predictions */} - {priceHistory.length > 0 && ( -
-
-
- -

Price History & AI Predictions

-
-
-
- -
-
- )} + {/* Tab Navigation */} +
+ {TABS.map(tab => { + const Icon = tab.icon; + const isActive = activeTab === tab.id; + return ( + + ); + })} - {/* AI Analysis Panel */} - {analysis && getRawAnalysis(symbol || '') && ( - - )} + {/* Action Buttons - Show on non-overview tabs */} + {activeTab !== 'overview' && ( +
+ {lastRefresh && ( + + Updated: {lastRefresh} + + )} - {/* Compact Stats Grid */} -
-
-
{history.length}
-
Analyses
-
-
-
- {history.filter((h: { decision: string }) => h.decision === 'BUY').length} -
-
Buy
-
-
-
- {history.filter((h: { decision: string }) => h.decision === 'HOLD').length} -
-
Hold
-
-
-
- {history.filter((h: { decision: string }) => h.decision === 'SELL').length} -
-
Sell
-
-
+ {/* Run Analysis Button */} + - {/* Analysis History */} -
-
-

Recommendation History

-
- - {history.length > 0 ? ( -
- {history.map((entry, idx) => ( -
-
- {new Date(entry.date).toLocaleDateString('en-IN', { - weekday: 'short', - month: 'short', - day: 'numeric', - })} -
- -
- ))} -
- ) : ( -
- -

No history yet

+ {/* Refresh Button */} +
)} -
+
- {/* Top Pick / Avoid Status - Compact */} + {/* Analysis Progress Banner */} + {analysisProgress && ( +
+ {isAnalysisRunning && } + {analysisProgress} +
+ )} + + {/* Refresh Notification */} + {refreshMessage && !analysisProgress && ( +
+ {refreshMessage} +
+ )} + + {/* Tab Content */} + {activeTab === 'overview' && ( + <> + {/* Price Chart with Predictions */} + {priceHistory.length > 0 && ( +
+
+
+ +

Price History & AI Predictions

+
+
+
+ +
+
+ )} + + {/* AI Analysis Panel */} + {analysis && getRawAnalysis(symbol || '') && ( + + )} + + {/* Compact Stats Grid */} +
+
+
{history.length}
+
Analyses
+
+
+
+ {history.filter((h: { decision: string }) => h.decision === 'BUY').length} +
+
Buy
+
+
+
+ {history.filter((h: { decision: string }) => h.decision === 'HOLD').length} +
+
Hold
+
+
+
+ {history.filter((h: { decision: string }) => h.decision === 'SELL').length} +
+
Sell
+
+
+ + {/* Analysis History */} +
+
+

Recommendation History

+
+ + {history.length > 0 ? ( +
+ {history.map((entry, idx) => ( +
+
+ {new Date(entry.date).toLocaleDateString('en-IN', { + weekday: 'short', + month: 'short', + day: 'numeric', + })} +
+ +
+ ))} +
+ ) : ( +
+ +

No history yet

+
+ )} +
+ + )} + + {activeTab === 'pipeline' && ( +
+ {/* Pipeline Overview */} +
+
+ +

Analysis Pipeline

+
+ console.log('Step clicked:', step)} + /> +
+ + {/* Agent Reports Grid */} +
+
+ +

Agent Reports

+
+
+ {(['market', 'news', 'social_media', 'fundamentals'] as AgentType[]).map(agentType => ( + + ))} +
+
+
+ )} + + {activeTab === 'debates' && ( +
+ {/* Investment Debate */} + + + {/* Risk Debate */} + +
+ )} + + {activeTab === 'data' && ( +
+ + + {/* No data message */} + {!isLoadingPipeline && (!pipelineData?.data_sources || pipelineData.data_sources.length === 0) && ( +
+ +

+ No Data Source Logs Available +

+

+ Data source logs will appear here when the analysis pipeline runs. + This includes information about market data, news, and fundamental data fetched. +

+
+ )} +
+ )} + + {/* Top Pick / Avoid Status - Compact (visible on all tabs) */} {latestRecommendation && ( <> {latestRecommendation.top_picks.some(p => p.symbol === symbol) && ( diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 1bb884cc..b9201fc2 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,8 +1,28 @@ /** * API service for fetching stock recommendations from the backend. + * Updated with cache-busting for refresh functionality. */ -const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; +import type { + FullPipelineData, + AgentReportsMap, + DebatesMap, + DataSourceLog, + PipelineSummary +} from '../types/pipeline'; + +// Use same hostname as the page, just different port for API +const getApiBaseUrl = () => { + // If env variable is set, use it + if (import.meta.env.VITE_API_URL) { + return import.meta.env.VITE_API_URL; + } + // Otherwise use the same host as the current page with port 8001 + const hostname = typeof window !== 'undefined' ? window.location.hostname : 'localhost'; + return `http://${hostname}:8001`; +}; + +const API_BASE_URL = getApiBaseUrl(); export interface StockAnalysis { symbol: string; @@ -57,14 +77,26 @@ class ApiService { this.baseUrl = API_BASE_URL; } - private async fetch(endpoint: string, options?: RequestInit): Promise { - const url = `${this.baseUrl}${endpoint}`; + private async fetch(endpoint: string, options?: RequestInit & { noCache?: boolean }): Promise { + let url = `${this.baseUrl}${endpoint}`; + + // Add cache-busting query param if noCache is true + const noCache = options?.noCache; + if (noCache) { + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}_t=${Date.now()}`; + } + + // Remove noCache from options before passing to fetch + const { noCache: _, ...fetchOptions } = options || {}; + const response = await fetch(url, { - ...options, + ...fetchOptions, headers: { 'Content-Type': 'application/json', - ...options?.headers, + ...fetchOptions?.headers, }, + cache: noCache ? 'no-store' : undefined, }); if (!response.ok) { @@ -131,6 +163,127 @@ class ApiService { body: JSON.stringify(recommendation), }); } + + // ============== Pipeline Data Methods ============== + + /** + * Get full pipeline data for a stock on a specific date + */ + async getPipelineData(date: string, symbol: string, refresh = false): Promise { + return this.fetch(`/recommendations/${date}/${symbol}/pipeline`, { noCache: refresh }); + } + + /** + * Get agent reports for a stock on a specific date + */ + async getAgentReports(date: string, symbol: string): Promise<{ + date: string; + symbol: string; + reports: AgentReportsMap; + count: number; + }> { + return this.fetch(`/recommendations/${date}/${symbol}/agents`); + } + + /** + * Get debate history for a stock on a specific date + */ + async getDebateHistory(date: string, symbol: string): Promise<{ + date: string; + symbol: string; + debates: DebatesMap; + }> { + return this.fetch(`/recommendations/${date}/${symbol}/debates`); + } + + /** + * Get data source logs for a stock on a specific date + */ + async getDataSources(date: string, symbol: string): Promise<{ + date: string; + symbol: string; + data_sources: DataSourceLog[]; + count: number; + }> { + return this.fetch(`/recommendations/${date}/${symbol}/data-sources`); + } + + /** + * Get pipeline summary for all stocks on a specific date + */ + async getPipelineSummary(date: string): Promise<{ + date: string; + stocks: PipelineSummary[]; + count: number; + }> { + return this.fetch(`/recommendations/${date}/pipeline-summary`); + } + + /** + * Save pipeline data for a stock (used by the analyzer) + */ + async savePipelineData(data: { + date: string; + symbol: string; + agent_reports?: Record; + investment_debate?: Record; + risk_debate?: Record; + pipeline_steps?: unknown[]; + data_sources?: unknown[]; + }): Promise<{ message: string }> { + return this.fetch('/pipeline', { + method: 'POST', + body: JSON.stringify(data), + }); + } + + // ============== Analysis Trigger Methods ============== + + /** + * Start analysis for a stock + */ + async runAnalysis(symbol: string, date?: string): Promise<{ + message: string; + symbol: string; + date: string; + status: string; + }> { + const url = date ? `/analyze/${symbol}?date=${date}` : `/analyze/${symbol}`; + return this.fetch(url, { + method: 'POST', + body: JSON.stringify({}), + noCache: true, + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache' + } + }); + } + + /** + * Get analysis status for a stock + */ + async getAnalysisStatus(symbol: string): Promise<{ + symbol: string; + status: string; + progress?: string; + error?: string; + decision?: string; + started_at?: string; + completed_at?: string; + }> { + return this.fetch(`/analyze/${symbol}/status`, { noCache: true }); + } + + /** + * Get all running analyses + */ + async getRunningAnalyses(): Promise<{ + running: Record; + count: number; + }> { + return this.fetch('/analyze/running', { noCache: true }); + } } export const api = new ApiService(); diff --git a/frontend/src/types/pipeline.ts b/frontend/src/types/pipeline.ts new file mode 100644 index 00000000..7354aa02 --- /dev/null +++ b/frontend/src/types/pipeline.ts @@ -0,0 +1,199 @@ +/** + * TypeScript types for the analysis pipeline visualization + */ + +// Agent types that perform analysis +export type AgentType = 'market' | 'news' | 'social_media' | 'fundamentals'; + +// Debate types in the system +export type DebateType = 'investment' | 'risk'; + +// Pipeline step status +export type PipelineStepStatus = 'pending' | 'running' | 'completed' | 'error'; + +/** + * Individual agent's analysis report + */ +export interface AgentReport { + agent_type: AgentType; + report_content: string; + data_sources_used: string[]; + created_at?: string; +} + +/** + * Map of agent reports by type + */ +export interface AgentReportsMap { + market?: AgentReport; + news?: AgentReport; + social_media?: AgentReport; + fundamentals?: AgentReport; +} + +/** + * Debate history for investment or risk debates + */ +export interface DebateHistory { + debate_type: DebateType; + // Investment debate fields + bull_arguments?: string; + bear_arguments?: string; + // Risk debate fields + risky_arguments?: string; + safe_arguments?: string; + neutral_arguments?: string; + // Common fields + judge_decision?: string; + full_history?: string; + created_at?: string; +} + +/** + * Map of debates by type + */ +export interface DebatesMap { + investment?: DebateHistory; + risk?: DebateHistory; +} + +/** + * Single step in the analysis pipeline + */ +export interface PipelineStep { + step_number: number; + step_name: string; + status: PipelineStepStatus; + started_at?: string; + completed_at?: string; + duration_ms?: number; + output_summary?: string; +} + +/** + * Log entry for a data source fetch + */ +export interface DataSourceLog { + source_type: string; + source_name: string; + data_fetched?: Record; + fetch_timestamp?: string; + success: boolean; + error_message?: string; +} + +/** + * Complete pipeline data for a single stock analysis + */ +export interface FullPipelineData { + date: string; + symbol: string; + agent_reports: AgentReportsMap; + debates: DebatesMap; + pipeline_steps: PipelineStep[]; + data_sources: DataSourceLog[]; + status?: 'complete' | 'in_progress' | 'no_data'; +} + +/** + * Summary of pipeline for a single stock (used in list views) + */ +export interface PipelineSummary { + symbol: string; + pipeline_steps: { step_name: string; status: PipelineStepStatus }[]; + agent_reports_count: number; + has_debates: boolean; +} + +/** + * API response types + */ +export interface PipelineDataResponse extends FullPipelineData {} + +export interface AgentReportsResponse { + date: string; + symbol: string; + reports: AgentReportsMap; + count: number; +} + +export interface DebateHistoryResponse { + date: string; + symbol: string; + debates: DebatesMap; +} + +export interface DataSourcesResponse { + date: string; + symbol: string; + data_sources: DataSourceLog[]; + count: number; +} + +export interface PipelineSummaryResponse { + date: string; + stocks: PipelineSummary[]; + count: number; +} + +/** + * Pipeline step definitions (for UI rendering) + */ +export const PIPELINE_STEPS = [ + { number: 1, name: 'data_collection', label: 'Data Collection', icon: 'Database' }, + { number: 2, name: 'market_analysis', label: 'Market Analysis', icon: 'TrendingUp' }, + { number: 3, name: 'news_analysis', label: 'News Analysis', icon: 'Newspaper' }, + { number: 4, name: 'social_analysis', label: 'Social Analysis', icon: 'Users' }, + { number: 5, name: 'fundamentals_analysis', label: 'Fundamentals', icon: 'FileText' }, + { number: 6, name: 'investment_debate', label: 'Investment Debate', icon: 'MessageSquare' }, + { number: 7, name: 'trader_decision', label: 'Trader Decision', icon: 'Target' }, + { number: 8, name: 'risk_debate', label: 'Risk Assessment', icon: 'Shield' }, + { number: 9, name: 'final_decision', label: 'Final Decision', icon: 'CheckCircle' }, +] as const; + +/** + * Agent metadata for UI rendering + */ +export const AGENT_METADATA: Record = { + market: { + label: 'Market Analyst', + icon: 'TrendingUp', + color: 'blue', + description: 'Analyzes technical indicators, price trends, and market patterns' + }, + news: { + label: 'News Analyst', + icon: 'Newspaper', + color: 'purple', + description: 'Analyzes company news, macroeconomic trends, and market events' + }, + social_media: { + label: 'Social Media Analyst', + icon: 'Users', + color: 'pink', + description: 'Analyzes social sentiment, Reddit discussions, and public perception' + }, + fundamentals: { + label: 'Fundamentals Analyst', + icon: 'FileText', + color: 'green', + description: 'Analyzes financial statements, ratios, and company health' + } +}; + +/** + * Debate role metadata for UI rendering + */ +export const DEBATE_ROLES = { + investment: { + bull: { label: 'Bull Analyst', color: 'green', icon: 'TrendingUp' }, + bear: { label: 'Bear Analyst', color: 'red', icon: 'TrendingDown' }, + judge: { label: 'Research Manager', color: 'blue', icon: 'Scale' } + }, + risk: { + risky: { label: 'Aggressive Analyst', color: 'red', icon: 'Zap' }, + safe: { label: 'Conservative Analyst', color: 'green', icon: 'Shield' }, + neutral: { label: 'Neutral Analyst', color: 'gray', icon: 'Scale' }, + judge: { label: 'Risk Manager', color: 'blue', icon: 'ShieldCheck' } + } +} as const;