This commit is contained in:
parent
df916f1c1a
commit
cf1f89adf7
|
|
@ -59,6 +59,85 @@ def init_db():
|
||||||
CREATE INDEX IF NOT EXISTS idx_stock_analysis_symbol ON stock_analysis(symbol)
|
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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
@ -219,5 +298,393 @@ def get_all_recommendations() -> list:
|
||||||
return [get_recommendation_by_date(date) for date in dates]
|
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
|
# Initialize database on module import
|
||||||
init_db()
|
init_db()
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,9 +1,21 @@
|
||||||
"""FastAPI server for Nifty50 AI recommendations."""
|
"""FastAPI server for Nifty50 AI recommendations."""
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import database as db
|
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(
|
app = FastAPI(
|
||||||
title="Nifty50 AI API",
|
title="Nifty50 AI API",
|
||||||
|
|
@ -68,19 +80,131 @@ class SaveRecommendationRequest(BaseModel):
|
||||||
stocks_to_avoid: list
|
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("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
"""API root endpoint."""
|
"""API root endpoint."""
|
||||||
return {
|
return {
|
||||||
"name": "Nifty50 AI API",
|
"name": "Nifty50 AI API",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"GET /recommendations": "Get all recommendations",
|
"GET /recommendations": "Get all recommendations",
|
||||||
"GET /recommendations/latest": "Get latest recommendation",
|
"GET /recommendations/latest": "Get latest recommendation",
|
||||||
"GET /recommendations/{date}": "Get recommendation by date",
|
"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 /stocks/{symbol}/history": "Get stock history",
|
||||||
"GET /dates": "Get all available dates",
|
"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"}
|
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__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
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-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
|
"playwright": "^1.58.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"puppeteer": "^24.36.1",
|
"puppeteer": "^24.36.1",
|
||||||
"tailwindcss": "^4.1.18",
|
"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 { useParams, Link } from 'react-router-dom';
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState, useEffect } from 'react';
|
||||||
import { ArrowLeft, Building2, TrendingUp, TrendingDown, Minus, AlertTriangle, Calendar, Activity, LineChart } from 'lucide-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 { NIFTY_50_STOCKS } from '../types';
|
||||||
import { sampleRecommendations, getStockHistory, getExtendedPriceHistory, getPredictionPointsWithPrices, getRawAnalysis } from '../data/recommendations';
|
import { sampleRecommendations, getStockHistory, getExtendedPriceHistory, getPredictionPointsWithPrices, getRawAnalysis } from '../data/recommendations';
|
||||||
import { DecisionBadge, ConfidenceBadge, RiskBadge } from '../components/StockCard';
|
import { DecisionBadge, ConfidenceBadge, RiskBadge } from '../components/StockCard';
|
||||||
import AIAnalysisPanel from '../components/AIAnalysisPanel';
|
import AIAnalysisPanel from '../components/AIAnalysisPanel';
|
||||||
import StockPriceChart from '../components/StockPriceChart';
|
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() {
|
export default function StockDetail() {
|
||||||
const { symbol } = useParams<{ symbol: string }>();
|
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 stock = NIFTY_50_STOCKS.find(s => s.symbol === symbol);
|
||||||
const latestRecommendation = sampleRecommendations[0];
|
const latestRecommendation = sampleRecommendations[0];
|
||||||
|
|
@ -26,6 +52,119 @@ export default function StockDetail() {
|
||||||
: [];
|
: [];
|
||||||
}, [symbol, priceHistory]);
|
}, [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) {
|
if (!stock) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[60vh] flex items-center justify-center">
|
<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 DecisionIcon = analysis?.decision ? decisionIcon[analysis.decision] : Activity;
|
||||||
const bgGradient = analysis?.decision ? decisionColor[analysis.decision] : 'from-gray-500 to-gray-600';
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
|
|
@ -120,89 +266,266 @@ export default function StockDetail() {
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Price Chart with Predictions */}
|
{/* Tab Navigation */}
|
||||||
{priceHistory.length > 0 && (
|
<div className="card p-1 flex gap-1 overflow-x-auto">
|
||||||
<section className="card overflow-hidden">
|
{TABS.map(tab => {
|
||||||
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-800/50">
|
const Icon = tab.icon;
|
||||||
<div className="flex items-center gap-2">
|
const isActive = activeTab === tab.id;
|
||||||
<LineChart className="w-4 h-4 text-nifty-600 dark:text-nifty-400" />
|
return (
|
||||||
<h2 className="font-semibold text-gray-900 dark:text-gray-100 text-sm">Price History & AI Predictions</h2>
|
<button
|
||||||
</div>
|
key={tab.id}
|
||||||
</div>
|
onClick={() => setActiveTab(tab.id)}
|
||||||
<div className="p-4 bg-white dark:bg-slate-800">
|
className={`
|
||||||
<StockPriceChart
|
flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all whitespace-nowrap
|
||||||
priceHistory={priceHistory}
|
${isActive
|
||||||
predictions={predictionPoints}
|
? 'bg-nifty-600 text-white shadow-md'
|
||||||
symbol={symbol || ''}
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700'
|
||||||
/>
|
}
|
||||||
</div>
|
`}
|
||||||
</section>
|
>
|
||||||
)}
|
<Icon className="w-4 h-4" />
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* AI Analysis Panel */}
|
{/* Action Buttons - Show on non-overview tabs */}
|
||||||
{analysis && getRawAnalysis(symbol || '') && (
|
{activeTab !== 'overview' && (
|
||||||
<AIAnalysisPanel
|
<div className="ml-auto flex items-center gap-2">
|
||||||
analysis={getRawAnalysis(symbol || '') || ''}
|
{lastRefresh && (
|
||||||
decision={analysis.decision}
|
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
/>
|
Updated: {lastRefresh}
|
||||||
)}
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Compact Stats Grid */}
|
{/* Run Analysis Button */}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
<button
|
||||||
<div className="card p-2.5 text-center">
|
onClick={handleRunAnalysis}
|
||||||
<div className="text-lg font-bold text-gray-900 dark:text-gray-100">{history.length}</div>
|
disabled={isAnalysisRunning || isRefreshing || isLoadingPipeline}
|
||||||
<div className="text-[10px] text-gray-500 dark:text-gray-400">Analyses</div>
|
className={`
|
||||||
</div>
|
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
|
||||||
<div className="card p-2.5 text-center">
|
${isAnalysisRunning
|
||||||
<div className="text-lg font-bold text-green-600 dark:text-green-400">
|
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||||
{history.filter((h: { decision: string }) => h.decision === 'BUY').length}
|
: 'bg-nifty-600 text-white hover:bg-nifty-700'
|
||||||
</div>
|
}
|
||||||
<div className="text-[10px] text-gray-500 dark:text-gray-400">Buy</div>
|
disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
</div>
|
`}
|
||||||
<div className="card p-2.5 text-center">
|
title="Run AI analysis for this stock"
|
||||||
<div className="text-lg font-bold text-amber-600 dark:text-amber-400">
|
>
|
||||||
{history.filter((h: { decision: string }) => h.decision === 'HOLD').length}
|
{isAnalysisRunning ? (
|
||||||
</div>
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
<div className="text-[10px] text-gray-500 dark:text-gray-400">Hold</div>
|
) : (
|
||||||
</div>
|
<Play className="w-4 h-4" />
|
||||||
<div className="card p-2.5 text-center">
|
)}
|
||||||
<div className="text-lg font-bold text-red-600 dark:text-red-400">
|
{isAnalysisRunning ? 'Analyzing...' : 'Run Analysis'}
|
||||||
{history.filter((h: { decision: string }) => h.decision === 'SELL').length}
|
</button>
|
||||||
</div>
|
|
||||||
<div className="text-[10px] text-gray-500 dark:text-gray-400">Sell</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Analysis History */}
|
{/* Refresh Button */}
|
||||||
<section className="card">
|
<button
|
||||||
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-700/50">
|
onClick={handleRefresh}
|
||||||
<h2 className="font-semibold text-gray-900 dark:text-gray-100 text-sm">Recommendation History</h2>
|
disabled={isRefreshing || isLoadingPipeline || isAnalysisRunning}
|
||||||
</div>
|
className={`
|
||||||
|
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
|
||||||
{history.length > 0 ? (
|
text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700
|
||||||
<div className="divide-y divide-gray-50 dark:divide-slate-700 max-h-[250px] overflow-y-auto">
|
disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
{history.map((entry, idx) => (
|
`}
|
||||||
<div key={idx} className="px-3 py-2 flex items-center justify-between">
|
title="Refresh pipeline data"
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
>
|
||||||
{new Date(entry.date).toLocaleDateString('en-IN', {
|
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||||
weekday: 'short',
|
{isRefreshing ? 'Refreshing...' : 'Refresh'}
|
||||||
month: 'short',
|
</button>
|
||||||
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>
|
</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 && (
|
||||||
<>
|
<>
|
||||||
{latestRecommendation.top_picks.some(p => p.symbol === symbol) && (
|
{latestRecommendation.top_picks.some(p => p.symbol === symbol) && (
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,28 @@
|
||||||
/**
|
/**
|
||||||
* API service for fetching stock recommendations from the backend.
|
* 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 {
|
export interface StockAnalysis {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
|
@ -57,14 +77,26 @@ class ApiService {
|
||||||
this.baseUrl = API_BASE_URL;
|
this.baseUrl = API_BASE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetch<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
private async fetch<T>(endpoint: string, options?: RequestInit & { noCache?: boolean }): Promise<T> {
|
||||||
const url = `${this.baseUrl}${endpoint}`;
|
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, {
|
const response = await fetch(url, {
|
||||||
...options,
|
...fetchOptions,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...options?.headers,
|
...fetchOptions?.headers,
|
||||||
},
|
},
|
||||||
|
cache: noCache ? 'no-store' : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -131,6 +163,127 @@ class ApiService {
|
||||||
body: JSON.stringify(recommendation),
|
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();
|
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