This commit is contained in:
parent
92ff07a2b1
commit
9a292cde34
|
|
@ -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.
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export { PipelineOverview } from './PipelineOverview';
|
||||
export { AgentReportCard } from './AgentReportCard';
|
||||
export { DebateViewer } from './DebateViewer';
|
||||
export { RiskDebateViewer } from './RiskDebateViewer';
|
||||
export { DataSourcesPanel } from './DataSourcesPanel';
|
||||
|
|
@ -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) && (
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue