This commit is contained in:
hemangjoshi37a 2026-02-01 06:55:15 +11:00
parent 92ff07a2b1
commit 9a292cde34
13 changed files with 2560 additions and 88 deletions

View File

@ -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()

Binary file not shown.

View File

@ -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)

View File

@ -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",

View File

@ -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<AgentType, React.ElementType> = {
market: TrendingUp,
news: Newspaper,
social_media: Users,
fundamentals: FileText,
};
const AGENT_COLORS: Record<AgentType, { bg: string; border: string; text: string; accent: string }> = {
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 (
<div className={`rounded-xl border ${colors.border} ${colors.bg} overflow-hidden`}>
{/* Header */}
<div
className={`flex items-center justify-between p-4 cursor-pointer hover:opacity-90 transition-opacity`}
onClick={() => hasReport && setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${colors.accent} bg-opacity-20`}>
<Icon className={`w-5 h-5 ${colors.text}`} />
</div>
<div>
<h3 className={`font-semibold ${colors.text}`}>{metadata.label}</h3>
<p className="text-xs text-slate-500 dark:text-slate-400">
{metadata.description}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{hasReport ? (
<CheckCircle className="w-4 h-4 text-green-500" />
) : isLoading ? (
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin opacity-50" />
) : (
<Clock className="w-4 h-4 text-slate-400" />
)}
{hasReport && (
isExpanded ? (
<ChevronUp className="w-5 h-5 text-slate-400" />
) : (
<ChevronDown className="w-5 h-5 text-slate-400" />
)
)}
</div>
</div>
{/* Preview (collapsed) */}
{!isExpanded && hasReport && (
<div className="px-4 pb-4">
<p className="text-sm text-slate-600 dark:text-slate-400 line-clamp-2">
{previewText}
</p>
</div>
)}
{/* Expanded content */}
{isExpanded && hasReport && (
<div className="border-t border-slate-200 dark:border-slate-700">
{/* Data sources */}
{report.data_sources_used && report.data_sources_used.length > 0 && (
<div className="px-4 py-2 bg-slate-100 dark:bg-slate-800/50 flex items-center gap-2 flex-wrap">
<Database className="w-3 h-3 text-slate-400" />
<span className="text-xs text-slate-500">Sources:</span>
{report.data_sources_used.map((source, idx) => (
<span
key={idx}
className="px-2 py-0.5 bg-slate-200 dark:bg-slate-700 rounded text-xs text-slate-600 dark:text-slate-300"
>
{source}
</span>
))}
</div>
)}
{/* Report content */}
<div className="p-4 space-y-4 max-h-96 overflow-y-auto">
{sections.length > 0 ? (
sections.map((section, idx) => (
<div key={idx} className="space-y-1">
<h4 className="font-medium text-sm text-slate-700 dark:text-slate-300">
{section.title}
</h4>
<div className="text-sm text-slate-600 dark:text-slate-400 space-y-1">
{section.content.map((line, lineIdx) => (
<p key={lineIdx}>{line}</p>
))}
</div>
</div>
))
) : (
<div className="prose prose-sm dark:prose-invert max-w-none">
<pre className="whitespace-pre-wrap text-sm text-slate-600 dark:text-slate-400">
{report.report_content}
</pre>
</div>
)}
</div>
{/* Timestamp */}
{report.created_at && (
<div className="px-4 py-2 border-t border-slate-200 dark:border-slate-700 flex items-center gap-2">
<Clock className="w-3 h-3 text-slate-400" />
<span className="text-xs text-slate-500">
Generated: {new Date(report.created_at).toLocaleString()}
</span>
</div>
)}
</div>
)}
</div>
);
}
export default AgentReportCard;

View File

@ -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<string, { bg: string; text: string }> = {
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<Set<number>>(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 (
<div className="rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
{/* Header */}
<div
className="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-800/50 cursor-pointer"
onClick={() => hasData && setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-100 dark:bg-slate-700 rounded-lg">
<Database className="w-5 h-5 text-slate-600 dark:text-slate-300" />
</div>
<div>
<h3 className="font-semibold text-slate-800 dark:text-slate-200">
Data Sources
</h3>
<p className="text-xs text-slate-500">
Raw data fetched for analysis
</p>
</div>
</div>
<div className="flex items-center gap-2">
{hasData ? (
<div className="flex items-center gap-2">
<span className="flex items-center gap-1 px-2 py-1 bg-green-100 dark:bg-green-900/40 rounded text-xs text-green-700 dark:text-green-300">
<CheckCircle className="w-3 h-3" />
{successCount}
</span>
{errorCount > 0 && (
<span className="flex items-center gap-1 px-2 py-1 bg-red-100 dark:bg-red-900/40 rounded text-xs text-red-700 dark:text-red-300">
<XCircle className="w-3 h-3" />
{errorCount}
</span>
)}
</div>
) : isLoading ? (
<div className="w-4 h-4 border-2 border-slate-400 border-t-transparent rounded-full animate-spin" />
) : (
<span className="px-2 py-1 bg-slate-100 dark:bg-slate-800 rounded text-xs text-slate-500">
No Data
</span>
)}
{hasData && (
isExpanded ? <ChevronUp className="w-5 h-5 text-slate-400" /> : <ChevronDown className="w-5 h-5 text-slate-400" />
)}
</div>
</div>
{/* Expanded content */}
{isExpanded && hasData && (
<div className="border-t border-slate-200 dark:border-slate-700">
<div className="divide-y divide-slate-200 dark:divide-slate-700">
{dataSources.map((source, index) => {
const colors = getSourceColors(source.source_type);
const isSourceExpanded = expandedSources.has(index);
return (
<div key={index} className="bg-white dark:bg-slate-900">
{/* Source header */}
<div
className="flex items-center justify-between p-3 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/50"
onClick={() => toggleSourceExpanded(index)}
>
<div className="flex items-center gap-3">
<Server className="w-4 h-4 text-slate-400" />
<div>
<div className="flex items-center gap-2">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${colors.bg} ${colors.text}`}>
{source.source_type}
</span>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">
{source.source_name}
</span>
</div>
<div className="flex items-center gap-2 mt-1 text-xs text-slate-500">
<Clock className="w-3 h-3" />
{formatTimestamp(source.fetch_timestamp)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
{source.success ? (
<CheckCircle className="w-4 h-4 text-green-500" />
) : (
<XCircle className="w-4 h-4 text-red-500" />
)}
{isSourceExpanded ? (
<ChevronUp className="w-4 h-4 text-slate-400" />
) : (
<ChevronDown className="w-4 h-4 text-slate-400" />
)}
</div>
</div>
{/* Source details (expanded) */}
{isSourceExpanded && (
<div className="px-4 pb-4 border-t border-slate-100 dark:border-slate-800">
{source.error_message ? (
<div className="mt-3 p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
<p className="text-sm text-red-600 dark:text-red-400">
<strong>Error:</strong> {source.error_message}
</p>
</div>
) : source.data_fetched ? (
<div className="mt-3 p-3 bg-slate-50 dark:bg-slate-800 rounded-lg">
<p className="text-xs text-slate-500 mb-2 font-medium">
Data Summary:
</p>
<pre className="text-xs text-slate-600 dark:text-slate-400 overflow-x-auto max-h-40">
{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)}
</pre>
</div>
) : (
<p className="mt-3 text-sm text-slate-500">
No data details available
</p>
)}
</div>
)}
</div>
);
})}
</div>
</div>
)}
</div>
);
}
export default DataSourcesPanel;

View File

@ -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 (
<div className="rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
{/* Header */}
<div
className="flex items-center justify-between p-4 bg-gradient-to-r from-green-50 via-slate-50 to-red-50 dark:from-green-900/20 dark:via-slate-800 dark:to-red-900/20 cursor-pointer"
onClick={() => hasDebate && setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-3">
<div className="flex items-center -space-x-2">
<div className="p-2 bg-green-100 dark:bg-green-900/40 rounded-full border-2 border-white dark:border-slate-800">
<TrendingUp className="w-4 h-4 text-green-600" />
</div>
<div className="p-2 bg-slate-100 dark:bg-slate-700 rounded-full border-2 border-white dark:border-slate-800 z-10">
<Scale className="w-4 h-4 text-slate-600 dark:text-slate-300" />
</div>
<div className="p-2 bg-red-100 dark:bg-red-900/40 rounded-full border-2 border-white dark:border-slate-800">
<TrendingDown className="w-4 h-4 text-red-600" />
</div>
</div>
<div>
<h3 className="font-semibold text-slate-800 dark:text-slate-200">
Investment Debate
</h3>
<p className="text-xs text-slate-500">
Bull vs Bear Analysis with Research Manager Decision
</p>
</div>
</div>
<div className="flex items-center gap-2">
{hasDebate ? (
<span className="px-2 py-1 bg-green-100 dark:bg-green-900/40 rounded text-xs text-green-700 dark:text-green-300">
Complete
</span>
) : isLoading ? (
<div className="w-4 h-4 border-2 border-slate-400 border-t-transparent rounded-full animate-spin" />
) : (
<span className="px-2 py-1 bg-slate-100 dark:bg-slate-800 rounded text-xs text-slate-500">
No Data
</span>
)}
{hasDebate && (
isExpanded ? <ChevronUp className="w-5 h-5 text-slate-400" /> : <ChevronDown className="w-5 h-5 text-slate-400" />
)}
</div>
</div>
{/* Expanded content */}
{isExpanded && hasDebate && (
<div className="border-t border-slate-200 dark:border-slate-700">
{/* Tabs */}
<div className="flex border-b border-slate-200 dark:border-slate-700">
<button
onClick={() => setActiveTab('history')}
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
activeTab === 'history'
? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50 dark:bg-blue-900/20'
: 'text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'
}`}
>
<MessageSquare className="w-4 h-4 inline mr-2" />
Full Debate
</button>
<button
onClick={() => setActiveTab('bull')}
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
activeTab === 'bull'
? 'text-green-600 border-b-2 border-green-600 bg-green-50 dark:bg-green-900/20'
: 'text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'
}`}
>
<TrendingUp className="w-4 h-4 inline mr-2" />
Bull Case
</button>
<button
onClick={() => setActiveTab('bear')}
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
activeTab === 'bear'
? 'text-red-600 border-b-2 border-red-600 bg-red-50 dark:bg-red-900/20'
: 'text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'
}`}
>
<TrendingDown className="w-4 h-4 inline mr-2" />
Bear Case
</button>
</div>
{/* Content */}
<div className="p-4 max-h-96 overflow-y-auto">
{activeTab === 'history' && (
<div className="space-y-4">
{debateRounds.length > 0 ? (
debateRounds.map((round, idx) => {
const style = getSpeakerStyle(round.speaker);
const Icon = style.icon;
return (
<div
key={idx}
className={`${style.bg} border-l-4 ${style.border} rounded-r-lg p-3`}
>
<div className="flex items-center gap-2 mb-2">
<Icon className={`w-4 h-4 ${style.color}`} />
<span className={`font-medium text-sm ${style.color}`}>
{round.speaker}
</span>
</div>
<p className="text-sm text-slate-600 dark:text-slate-400 whitespace-pre-wrap">
{round.content}
</p>
</div>
);
})
) : debate.full_history ? (
<pre className="text-sm text-slate-600 dark:text-slate-400 whitespace-pre-wrap">
{debate.full_history}
</pre>
) : (
<p className="text-slate-500 text-sm">No debate history available</p>
)}
</div>
)}
{activeTab === 'bull' && (
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<TrendingUp className="w-5 h-5 text-green-600" />
<span className="font-medium text-green-700 dark:text-green-300">
Bull Analyst Arguments
</span>
</div>
<p className="text-sm text-slate-600 dark:text-slate-400 whitespace-pre-wrap">
{debate.bull_arguments || 'No bull arguments recorded'}
</p>
</div>
)}
{activeTab === 'bear' && (
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<TrendingDown className="w-5 h-5 text-red-600" />
<span className="font-medium text-red-700 dark:text-red-300">
Bear Analyst Arguments
</span>
</div>
<p className="text-sm text-slate-600 dark:text-slate-400 whitespace-pre-wrap">
{debate.bear_arguments || 'No bear arguments recorded'}
</p>
</div>
)}
</div>
{/* Judge Decision */}
{debate.judge_decision && (
<div className="border-t border-slate-200 dark:border-slate-700 p-4 bg-blue-50 dark:bg-blue-900/20">
<div className="flex items-start gap-3">
<Award className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<h4 className="font-medium text-blue-700 dark:text-blue-300 mb-1">
Research Manager Decision
</h4>
<p className="text-sm text-slate-600 dark:text-slate-400 whitespace-pre-wrap">
{debate.judge_decision}
</p>
</div>
</div>
</div>
)}
</div>
)}
</div>
);
}
export default DebateViewer;

View File

@ -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<string, React.ElementType> = {
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<string, string> = {
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<PipelineStepStatus, { bg: string; border: string; text: string; icon?: React.ElementType }> = {
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 (
<div className="flex items-center gap-1">
{displaySteps.map((step, index) => {
const styles = STATUS_STYLES[step.status];
return (
<div
key={step.step_number}
className={`w-2 h-2 rounded-full ${styles.bg} ${styles.border} border`}
title={`${STEP_LABELS[step.step_name] || step.step_name}: ${step.status}`}
/>
);
})}
<span className="text-xs text-slate-500 ml-1">{progress}%</span>
</div>
);
}
return (
<div className="space-y-4">
{/* Progress bar */}
<div className="flex items-center gap-3">
<div className="flex-1 h-2 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-blue-500 to-green-500 transition-all duration-500"
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">
{completedCount}/{totalSteps}
</span>
</div>
{/* Pipeline steps */}
<div className="flex flex-wrap gap-2">
{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 (
<button
key={step.step_number}
onClick={() => onStepClick?.(step)}
className={`
flex items-center gap-2 px-3 py-2 rounded-lg border-2 transition-all
${styles.bg} ${styles.border} ${styles.text}
hover:scale-105 hover:shadow-md
${onStepClick ? 'cursor-pointer' : 'cursor-default'}
`}
>
<div className="relative">
<StepIcon className="w-4 h-4" />
{StatusIcon && step.status === 'running' && (
<Loader2 className="w-3 h-3 absolute -top-1 -right-1 animate-spin" />
)}
</div>
<span className="text-xs font-medium">{label}</span>
{step.duration_ms && (
<span className="text-xs opacity-60">
{(step.duration_ms / 1000).toFixed(1)}s
</span>
)}
</button>
);
})}
</div>
</div>
);
}
export default PipelineOverview;

View File

@ -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 (
<div className="rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
{/* Header */}
<div
className="flex items-center justify-between p-4 bg-gradient-to-r from-red-50 via-slate-50 to-green-50 dark:from-red-900/20 dark:via-slate-800 dark:to-green-900/20 cursor-pointer"
onClick={() => hasDebate && setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-3">
<div className="flex items-center -space-x-2">
<div className="p-2 bg-red-100 dark:bg-red-900/40 rounded-full border-2 border-white dark:border-slate-800">
<Zap className="w-4 h-4 text-red-600" />
</div>
<div className="p-2 bg-slate-100 dark:bg-slate-700 rounded-full border-2 border-white dark:border-slate-800 z-10">
<Scale className="w-4 h-4 text-slate-600 dark:text-slate-300" />
</div>
<div className="p-2 bg-green-100 dark:bg-green-900/40 rounded-full border-2 border-white dark:border-slate-800">
<Shield className="w-4 h-4 text-green-600" />
</div>
</div>
<div>
<h3 className="font-semibold text-slate-800 dark:text-slate-200">
Risk Assessment Debate
</h3>
<p className="text-xs text-slate-500">
Aggressive vs Conservative vs Neutral with Risk Manager Decision
</p>
</div>
</div>
<div className="flex items-center gap-2">
{hasDebate ? (
<span className="px-2 py-1 bg-green-100 dark:bg-green-900/40 rounded text-xs text-green-700 dark:text-green-300">
Complete
</span>
) : isLoading ? (
<div className="w-4 h-4 border-2 border-slate-400 border-t-transparent rounded-full animate-spin" />
) : (
<span className="px-2 py-1 bg-slate-100 dark:bg-slate-800 rounded text-xs text-slate-500">
No Data
</span>
)}
{hasDebate && (
isExpanded ? <ChevronUp className="w-5 h-5 text-slate-400" /> : <ChevronDown className="w-5 h-5 text-slate-400" />
)}
</div>
</div>
{/* Expanded content */}
{isExpanded && hasDebate && (
<div className="border-t border-slate-200 dark:border-slate-700">
{/* Tabs */}
<div className="flex border-b border-slate-200 dark:border-slate-700 overflow-x-auto">
<button
onClick={() => setActiveTab('all')}
className={`flex-1 min-w-fit px-4 py-2 text-sm font-medium transition-colors ${
activeTab === 'all'
? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50 dark:bg-blue-900/20'
: 'text-slate-500 hover:text-slate-700'
}`}
>
All Views
</button>
<button
onClick={() => setActiveTab('risky')}
className={`flex-1 min-w-fit px-4 py-2 text-sm font-medium transition-colors ${
activeTab === 'risky'
? 'text-red-600 border-b-2 border-red-600 bg-red-50 dark:bg-red-900/20'
: 'text-slate-500 hover:text-slate-700'
}`}
>
<Zap className="w-4 h-4 inline mr-1" />
Aggressive
</button>
<button
onClick={() => setActiveTab('neutral')}
className={`flex-1 min-w-fit px-4 py-2 text-sm font-medium transition-colors ${
activeTab === 'neutral'
? 'text-slate-600 border-b-2 border-slate-600 bg-slate-50 dark:bg-slate-900/20'
: 'text-slate-500 hover:text-slate-700'
}`}
>
<Scale className="w-4 h-4 inline mr-1" />
Neutral
</button>
<button
onClick={() => setActiveTab('safe')}
className={`flex-1 min-w-fit px-4 py-2 text-sm font-medium transition-colors ${
activeTab === 'safe'
? 'text-green-600 border-b-2 border-green-600 bg-green-50 dark:bg-green-900/20'
: 'text-slate-500 hover:text-slate-700'
}`}
>
<Shield className="w-4 h-4 inline mr-1" />
Conservative
</button>
</div>
{/* Content */}
<div className="p-4 max-h-96 overflow-y-auto">
{activeTab === 'all' && (
<div className="grid gap-4 md:grid-cols-3">
{/* Aggressive */}
<div className={`${ROLE_STYLES.risky.bg} rounded-lg p-3 border-l-4 ${ROLE_STYLES.risky.border}`}>
<div className="flex items-center gap-2 mb-2">
<Zap className={`w-4 h-4 ${ROLE_STYLES.risky.color}`} />
<span className={`font-medium text-sm ${ROLE_STYLES.risky.color}`}>
{ROLE_STYLES.risky.label}
</span>
</div>
<p className="text-xs text-slate-600 dark:text-slate-400 line-clamp-6">
{debate.risky_arguments || 'No arguments recorded'}
</p>
</div>
{/* Neutral */}
<div className={`${ROLE_STYLES.neutral.bg} rounded-lg p-3 border-l-4 ${ROLE_STYLES.neutral.border}`}>
<div className="flex items-center gap-2 mb-2">
<Scale className={`w-4 h-4 ${ROLE_STYLES.neutral.color}`} />
<span className={`font-medium text-sm ${ROLE_STYLES.neutral.color}`}>
{ROLE_STYLES.neutral.label}
</span>
</div>
<p className="text-xs text-slate-600 dark:text-slate-400 line-clamp-6">
{debate.neutral_arguments || 'No arguments recorded'}
</p>
</div>
{/* Conservative */}
<div className={`${ROLE_STYLES.safe.bg} rounded-lg p-3 border-l-4 ${ROLE_STYLES.safe.border}`}>
<div className="flex items-center gap-2 mb-2">
<Shield className={`w-4 h-4 ${ROLE_STYLES.safe.color}`} />
<span className={`font-medium text-sm ${ROLE_STYLES.safe.color}`}>
{ROLE_STYLES.safe.label}
</span>
</div>
<p className="text-xs text-slate-600 dark:text-slate-400 line-clamp-6">
{debate.safe_arguments || 'No arguments recorded'}
</p>
</div>
</div>
)}
{activeTab === 'risky' && (
<div className={`${ROLE_STYLES.risky.bg} rounded-lg p-4`}>
<div className="flex items-center gap-2 mb-3">
<Zap className={`w-5 h-5 ${ROLE_STYLES.risky.color}`} />
<span className={`font-medium ${ROLE_STYLES.risky.color}`}>
{ROLE_STYLES.risky.label}
</span>
<AlertTriangle className="w-4 h-4 text-amber-500 ml-auto" />
</div>
<p className="text-sm text-slate-600 dark:text-slate-400 whitespace-pre-wrap">
{debate.risky_arguments || 'No aggressive arguments recorded'}
</p>
</div>
)}
{activeTab === 'neutral' && (
<div className={`${ROLE_STYLES.neutral.bg} rounded-lg p-4`}>
<div className="flex items-center gap-2 mb-3">
<Scale className={`w-5 h-5 ${ROLE_STYLES.neutral.color}`} />
<span className={`font-medium ${ROLE_STYLES.neutral.color}`}>
{ROLE_STYLES.neutral.label}
</span>
</div>
<p className="text-sm text-slate-600 dark:text-slate-400 whitespace-pre-wrap">
{debate.neutral_arguments || 'No neutral arguments recorded'}
</p>
</div>
)}
{activeTab === 'safe' && (
<div className={`${ROLE_STYLES.safe.bg} rounded-lg p-4`}>
<div className="flex items-center gap-2 mb-3">
<Shield className={`w-5 h-5 ${ROLE_STYLES.safe.color}`} />
<span className={`font-medium ${ROLE_STYLES.safe.color}`}>
{ROLE_STYLES.safe.label}
</span>
</div>
<p className="text-sm text-slate-600 dark:text-slate-400 whitespace-pre-wrap">
{debate.safe_arguments || 'No conservative arguments recorded'}
</p>
</div>
)}
</div>
{/* Risk Manager Decision */}
{debate.judge_decision && (
<div className="border-t border-slate-200 dark:border-slate-700 p-4 bg-blue-50 dark:bg-blue-900/20">
<div className="flex items-start gap-3">
<ShieldCheck className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<h4 className="font-medium text-blue-700 dark:text-blue-300 mb-1">
Risk Manager Decision
</h4>
<p className="text-sm text-slate-600 dark:text-slate-400 whitespace-pre-wrap">
{debate.judge_decision}
</p>
</div>
</div>
</div>
)}
</div>
)}
</div>
);
}
export default RiskDebateViewer;

View File

@ -0,0 +1,5 @@
export { PipelineOverview } from './PipelineOverview';
export { AgentReportCard } from './AgentReportCard';
export { DebateViewer } from './DebateViewer';
export { RiskDebateViewer } from './RiskDebateViewer';
export { DataSourcesPanel } from './DataSourcesPanel';

View File

@ -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<TabType>('overview');
const [pipelineData, setPipelineData] = useState<FullPipelineData | null>(null);
const [isLoadingPipeline, setIsLoadingPipeline] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [lastRefresh, setLastRefresh] = useState<string | null>(null);
const [refreshMessage, setRefreshMessage] = useState<string | null>(null);
// Analysis state
const [isAnalysisRunning, setIsAnalysisRunning] = useState(false);
const [analysisStatus, setAnalysisStatus] = useState<string | null>(null);
const [analysisProgress, setAnalysisProgress] = useState<string | null>(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 (
<div className="min-h-[60vh] flex items-center justify-center">
@ -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 (
<div className="space-y-4">
{/* Back Button */}
@ -120,89 +266,266 @@ export default function StockDetail() {
)}
</section>
{/* Price Chart with Predictions */}
{priceHistory.length > 0 && (
<section className="card overflow-hidden">
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-800/50">
<div className="flex items-center gap-2">
<LineChart className="w-4 h-4 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100 text-sm">Price History & AI Predictions</h2>
</div>
</div>
<div className="p-4 bg-white dark:bg-slate-800">
<StockPriceChart
priceHistory={priceHistory}
predictions={predictionPoints}
symbol={symbol || ''}
/>
</div>
</section>
)}
{/* Tab Navigation */}
<div className="card p-1 flex gap-1 overflow-x-auto">
{TABS.map(tab => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all whitespace-nowrap
${isActive
? 'bg-nifty-600 text-white shadow-md'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700'
}
`}
>
<Icon className="w-4 h-4" />
{tab.label}
</button>
);
})}
{/* AI Analysis Panel */}
{analysis && getRawAnalysis(symbol || '') && (
<AIAnalysisPanel
analysis={getRawAnalysis(symbol || '') || ''}
decision={analysis.decision}
/>
)}
{/* Action Buttons - Show on non-overview tabs */}
{activeTab !== 'overview' && (
<div className="ml-auto flex items-center gap-2">
{lastRefresh && (
<span className="text-xs text-gray-400 dark:text-gray-500">
Updated: {lastRefresh}
</span>
)}
{/* Compact Stats Grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div className="card p-2.5 text-center">
<div className="text-lg font-bold text-gray-900 dark:text-gray-100">{history.length}</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Analyses</div>
</div>
<div className="card p-2.5 text-center">
<div className="text-lg font-bold text-green-600 dark:text-green-400">
{history.filter((h: { decision: string }) => h.decision === 'BUY').length}
</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Buy</div>
</div>
<div className="card p-2.5 text-center">
<div className="text-lg font-bold text-amber-600 dark:text-amber-400">
{history.filter((h: { decision: string }) => h.decision === 'HOLD').length}
</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Hold</div>
</div>
<div className="card p-2.5 text-center">
<div className="text-lg font-bold text-red-600 dark:text-red-400">
{history.filter((h: { decision: string }) => h.decision === 'SELL').length}
</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Sell</div>
</div>
</div>
{/* Run Analysis Button */}
<button
onClick={handleRunAnalysis}
disabled={isAnalysisRunning || isRefreshing || isLoadingPipeline}
className={`
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
${isAnalysisRunning
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
: 'bg-nifty-600 text-white hover:bg-nifty-700'
}
disabled:opacity-50 disabled:cursor-not-allowed
`}
title="Run AI analysis for this stock"
>
{isAnalysisRunning ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Play className="w-4 h-4" />
)}
{isAnalysisRunning ? 'Analyzing...' : 'Run Analysis'}
</button>
{/* Analysis History */}
<section className="card">
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-700/50">
<h2 className="font-semibold text-gray-900 dark:text-gray-100 text-sm">Recommendation History</h2>
</div>
{history.length > 0 ? (
<div className="divide-y divide-gray-50 dark:divide-slate-700 max-h-[250px] overflow-y-auto">
{history.map((entry, idx) => (
<div key={idx} className="px-3 py-2 flex items-center justify-between">
<div className="text-xs text-gray-500 dark:text-gray-400">
{new Date(entry.date).toLocaleDateString('en-IN', {
weekday: 'short',
month: 'short',
day: 'numeric',
})}
</div>
<DecisionBadge decision={entry.decision} size="small" />
</div>
))}
</div>
) : (
<div className="p-6 text-center">
<Calendar className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
<p className="text-sm text-gray-500 dark:text-gray-400">No history yet</p>
{/* Refresh Button */}
<button
onClick={handleRefresh}
disabled={isRefreshing || isLoadingPipeline || isAnalysisRunning}
className={`
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700
disabled:opacity-50 disabled:cursor-not-allowed
`}
title="Refresh pipeline data"
>
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? 'Refreshing...' : 'Refresh'}
</button>
</div>
)}
</section>
</div>
{/* Top Pick / Avoid Status - Compact */}
{/* Analysis Progress Banner */}
{analysisProgress && (
<div className={`p-3 rounded-lg text-sm font-medium flex items-center gap-2 ${
analysisStatus === 'completed'
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
: analysisStatus === 'error'
? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300'
: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'
}`}>
{isAnalysisRunning && <Loader2 className="w-4 h-4 animate-spin" />}
{analysisProgress}
</div>
)}
{/* Refresh Notification */}
{refreshMessage && !analysisProgress && (
<div className={`p-3 rounded-lg text-sm font-medium ${
refreshMessage.startsWith('✓')
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
: refreshMessage.startsWith('✗')
? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300'
: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300'
}`}>
{refreshMessage}
</div>
)}
{/* Tab Content */}
{activeTab === 'overview' && (
<>
{/* Price Chart with Predictions */}
{priceHistory.length > 0 && (
<section className="card overflow-hidden">
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-800/50">
<div className="flex items-center gap-2">
<LineChart className="w-4 h-4 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100 text-sm">Price History & AI Predictions</h2>
</div>
</div>
<div className="p-4 bg-white dark:bg-slate-800">
<StockPriceChart
priceHistory={priceHistory}
predictions={predictionPoints}
symbol={symbol || ''}
/>
</div>
</section>
)}
{/* AI Analysis Panel */}
{analysis && getRawAnalysis(symbol || '') && (
<AIAnalysisPanel
analysis={getRawAnalysis(symbol || '') || ''}
decision={analysis.decision}
/>
)}
{/* Compact Stats Grid */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div className="card p-2.5 text-center">
<div className="text-lg font-bold text-gray-900 dark:text-gray-100">{history.length}</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Analyses</div>
</div>
<div className="card p-2.5 text-center">
<div className="text-lg font-bold text-green-600 dark:text-green-400">
{history.filter((h: { decision: string }) => h.decision === 'BUY').length}
</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Buy</div>
</div>
<div className="card p-2.5 text-center">
<div className="text-lg font-bold text-amber-600 dark:text-amber-400">
{history.filter((h: { decision: string }) => h.decision === 'HOLD').length}
</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Hold</div>
</div>
<div className="card p-2.5 text-center">
<div className="text-lg font-bold text-red-600 dark:text-red-400">
{history.filter((h: { decision: string }) => h.decision === 'SELL').length}
</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400">Sell</div>
</div>
</div>
{/* Analysis History */}
<section className="card">
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-700/50">
<h2 className="font-semibold text-gray-900 dark:text-gray-100 text-sm">Recommendation History</h2>
</div>
{history.length > 0 ? (
<div className="divide-y divide-gray-50 dark:divide-slate-700 max-h-[250px] overflow-y-auto">
{history.map((entry, idx) => (
<div key={idx} className="px-3 py-2 flex items-center justify-between">
<div className="text-xs text-gray-500 dark:text-gray-400">
{new Date(entry.date).toLocaleDateString('en-IN', {
weekday: 'short',
month: 'short',
day: 'numeric',
})}
</div>
<DecisionBadge decision={entry.decision} size="small" />
</div>
))}
</div>
) : (
<div className="p-6 text-center">
<Calendar className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
<p className="text-sm text-gray-500 dark:text-gray-400">No history yet</p>
</div>
)}
</section>
</>
)}
{activeTab === 'pipeline' && (
<div className="space-y-4">
{/* Pipeline Overview */}
<section className="card p-4">
<div className="flex items-center gap-2 mb-4">
<Layers className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Analysis Pipeline</h2>
</div>
<PipelineOverview
steps={pipelineData?.pipeline_steps || []}
onStepClick={(step) => console.log('Step clicked:', step)}
/>
</section>
{/* Agent Reports Grid */}
<section className="card p-4">
<div className="flex items-center gap-2 mb-4">
<FileText className="w-5 h-5 text-nifty-600 dark:text-nifty-400" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Agent Reports</h2>
</div>
<div className="grid gap-4 md:grid-cols-2">
{(['market', 'news', 'social_media', 'fundamentals'] as AgentType[]).map(agentType => (
<AgentReportCard
key={agentType}
agentType={agentType}
report={pipelineData?.agent_reports?.[agentType]}
isLoading={isLoadingPipeline}
/>
))}
</div>
</section>
</div>
)}
{activeTab === 'debates' && (
<div className="space-y-4">
{/* Investment Debate */}
<DebateViewer
debate={pipelineData?.debates?.investment}
isLoading={isLoadingPipeline}
/>
{/* Risk Debate */}
<RiskDebateViewer
debate={pipelineData?.debates?.risk}
isLoading={isLoadingPipeline}
/>
</div>
)}
{activeTab === 'data' && (
<div className="space-y-4">
<DataSourcesPanel
dataSources={pipelineData?.data_sources || []}
isLoading={isLoadingPipeline}
/>
{/* No data message */}
{!isLoadingPipeline && (!pipelineData?.data_sources || pipelineData.data_sources.length === 0) && (
<div className="card p-8 text-center">
<Database className="w-12 h-12 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-2">
No Data Source Logs Available
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
Data source logs will appear here when the analysis pipeline runs.
This includes information about market data, news, and fundamental data fetched.
</p>
</div>
)}
</div>
)}
{/* Top Pick / Avoid Status - Compact (visible on all tabs) */}
{latestRecommendation && (
<>
{latestRecommendation.top_picks.some(p => p.symbol === symbol) && (

View File

@ -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<T>(endpoint: string, options?: RequestInit): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
private async fetch<T>(endpoint: string, options?: RequestInit & { noCache?: boolean }): Promise<T> {
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<FullPipelineData> {
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<string, unknown>;
investment_debate?: Record<string, unknown>;
risk_debate?: Record<string, unknown>;
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<string, unknown>;
count: number;
}> {
return this.fetch('/analyze/running', { noCache: true });
}
}
export const api = new ApiService();

View File

@ -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<string, unknown>;
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<AgentType, { label: string; icon: string; color: string; description: string }> = {
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;