Fix bugs
This commit is contained in:
parent
37e4a09022
commit
66b5ff0bff
|
|
@ -29,3 +29,4 @@ pydantic
|
||||||
python-multipart
|
python-multipart
|
||||||
python-jose[cryptography]
|
python-jose[cryptography]
|
||||||
passlib[bcrypt]
|
passlib[bcrypt]
|
||||||
|
python-dotenv
|
||||||
|
|
@ -190,11 +190,14 @@ class TradingAgentsGraph:
|
||||||
# Log state
|
# Log state
|
||||||
self._log_state(trade_date, final_state)
|
self._log_state(trade_date, final_state)
|
||||||
|
|
||||||
# Transform output JSON into widget-friendly format
|
eval_results_path=f"{RESULTS_BASE}"
|
||||||
data_transformation_agent = DataTransformationAgent(TransformationConfig(
|
input_file_path = f"{eval_results_path}/{company_name}/TradingAgentsStrategy_logs/full_states_log_{trade_date}.json"
|
||||||
eval_results_path=f"{RESULTS_BASE}/{company_name}/TradingAgentsStrategy_transformed_logs/full_states_log_{trade_date}.json"))
|
output_file_path = f"{eval_results_path}/{company_name}/TradingAgentsStrategy_transformed_logs/"
|
||||||
|
|
||||||
transformed_output = data_transformation_agent.transform_single_file(self._get_state(trade_date))
|
# Transform output JSON into widget-friendly format
|
||||||
|
data_transformation_agent = DataTransformationAgent(TransformationConfig(eval_results_path=eval_results_path))
|
||||||
|
|
||||||
|
transformed_output = data_transformation_agent.process_single_file(input_file_path, output_file_path)
|
||||||
|
|
||||||
# Return decision and processed signal
|
# Return decision and processed signal
|
||||||
return transformed_output, self.process_signal(final_state["final_trade_decision"])
|
return transformed_output, self.process_signal(final_state["final_trade_decision"])
|
||||||
|
|
@ -232,11 +235,12 @@ class TradingAgentsGraph:
|
||||||
}
|
}
|
||||||
|
|
||||||
# Save to file
|
# Save to file
|
||||||
directory = Path(f"../output_data/{self.ticker}/TradingAgentsStrategy_logs/")
|
output_directory_path = os.path.join(os.path.dirname(__file__), "..", "..", "output_data",f"{self.ticker}", "TradingAgentsStrategy_logs")
|
||||||
|
directory = Path(output_directory_path)
|
||||||
directory.mkdir(parents=True, exist_ok=True)
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
with open(
|
with open(
|
||||||
f"../output_data/{self.ticker}/TradingAgentsStrategy_logs/full_states_log_{trade_date}.json",
|
f"{output_directory_path}/full_states_log_{trade_date}.json",
|
||||||
"w",
|
"w",
|
||||||
) as f:
|
) as f:
|
||||||
json.dump(self.log_states_dict, f, indent=4)
|
json.dump(self.log_states_dict, f, indent=4)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,17 @@ import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import glob
|
import glob
|
||||||
import uuid
|
import uuid
|
||||||
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
|
# Load environment variables from .env (if present)
|
||||||
|
try:
|
||||||
|
from dotenv import load_dotenv, find_dotenv
|
||||||
|
_dotenv_path = find_dotenv()
|
||||||
|
if _dotenv_path:
|
||||||
|
load_dotenv(_dotenv_path)
|
||||||
|
except Exception:
|
||||||
|
# dotenv is optional; ignore if not installed
|
||||||
|
pass
|
||||||
|
|
||||||
# Import your TradingAgents components
|
# Import your TradingAgents components
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -19,6 +30,13 @@ app = FastAPI(title="TradingAgents API", version="1.0.0", debug=True)
|
||||||
# Centralized results directory to avoid repetition
|
# Centralized results directory to avoid repetition
|
||||||
RESULTS_BASE = os.path.join(os.path.dirname(__file__), "..", "..", "output_data")
|
RESULTS_BASE = os.path.join(os.path.dirname(__file__), "..", "..", "output_data")
|
||||||
|
|
||||||
|
# Simple startup check for OPENAI_API_KEY
|
||||||
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||||
|
if not OPENAI_API_KEY:
|
||||||
|
print("[WARN] OPENAI_API_KEY is not set. Set it in your shell or in a .env file.")
|
||||||
|
else:
|
||||||
|
print("[INFO] OPENAI_API_KEY detected from environment.")
|
||||||
|
|
||||||
# Configure CORS
|
# Configure CORS
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
|
|
@ -58,35 +76,34 @@ async def health_check():
|
||||||
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
|
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
|
||||||
|
|
||||||
async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config_overrides: Dict[str, Any] = None):
|
async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config_overrides: Dict[str, Any] = None):
|
||||||
"""Background task to run the trading analysis"""
|
"""Background task to run the trading analysis without blocking the event loop"""
|
||||||
try:
|
try:
|
||||||
jobs[job_id].status = "running"
|
jobs[job_id].status = "running"
|
||||||
jobs[job_id].progress = "Initializing TradingAgents..."
|
jobs[job_id].progress = "Initializing TradingAgents..."
|
||||||
|
|
||||||
# Create custom config
|
# Prepare config
|
||||||
config = DEFAULT_CONFIG.copy()
|
config = DEFAULT_CONFIG.copy()
|
||||||
if config_overrides:
|
if config_overrides:
|
||||||
config.update(config_overrides)
|
config.update(config_overrides)
|
||||||
|
|
||||||
# Initialize TradingAgents
|
|
||||||
jobs[job_id].progress = "Setting up trading graph..."
|
jobs[job_id].progress = "Setting up trading graph..."
|
||||||
|
|
||||||
# Do not set API keys in code. Use environment variables or a secure secret manager.
|
# Define blocking work as sync function
|
||||||
ta = TradingAgentsGraph(debug=True, config=config)
|
def _do_work():
|
||||||
|
ta = TradingAgentsGraph(debug=True, config=config)
|
||||||
|
jobs[job_id].progress = f"Analyzing {symbol} for {analysis_date}..."
|
||||||
|
_, decision = ta.propagate(symbol, analysis_date)
|
||||||
|
return decision
|
||||||
|
|
||||||
# Run the analysis
|
# Run blocking work in threadpool so the event loop stays responsive
|
||||||
jobs[job_id].progress = f"Analyzing {symbol} for {analysis_date}..."
|
decision = await run_in_threadpool(_do_work)
|
||||||
_, decision = ta.propagate(symbol, analysis_date)
|
|
||||||
|
|
||||||
print(_)
|
|
||||||
print("Decision: ", decision)
|
|
||||||
|
|
||||||
jobs[job_id].status = "completed"
|
jobs[job_id].status = "completed"
|
||||||
jobs[job_id].result = {
|
jobs[job_id].result = {
|
||||||
"symbol": symbol,
|
"symbol": symbol,
|
||||||
"date": analysis_date,
|
"date": analysis_date,
|
||||||
"decision": decision,
|
"decision": decision,
|
||||||
"completed_at": datetime.now().isoformat()
|
"completed_at": datetime.now().isoformat(),
|
||||||
}
|
}
|
||||||
jobs[job_id].progress = "Analysis completed successfully"
|
jobs[job_id].progress = "Analysis completed successfully"
|
||||||
|
|
||||||
|
|
@ -320,6 +337,20 @@ async def get_specific_transformed_result(symbol: str, date: str):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Error reading file: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Error reading file: {str(e)}")
|
||||||
|
|
||||||
|
@app.get("/jobs")
|
||||||
|
async def get_jobs():
|
||||||
|
"""Get all jobs"""
|
||||||
|
job_lst = []
|
||||||
|
for job_id, job in jobs.items():
|
||||||
|
job_lst.append({
|
||||||
|
"job_id": job_id,
|
||||||
|
"status": job.status,
|
||||||
|
"progress": job.progress,
|
||||||
|
"result": job.result,
|
||||||
|
"error": job.error
|
||||||
|
})
|
||||||
|
return {"jobs": job_lst}
|
||||||
|
|
||||||
@app.get("/config")
|
@app.get("/config")
|
||||||
async def get_default_config():
|
async def get_default_config():
|
||||||
"""Get the default configuration"""
|
"""Get the default configuration"""
|
||||||
|
|
@ -327,4 +358,4 @@ async def get_default_config():
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ function App() {
|
||||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||||
const [showWidgetsView, setShowWidgetsView] = useState(false);
|
const [showWidgetsView, setShowWidgetsView] = useState(false);
|
||||||
const [showTransformedDataModal, setShowTransformedDataModal] = useState(false);
|
const [showTransformedDataModal, setShowTransformedDataModal] = useState(false);
|
||||||
|
const [showJobsModal, setShowJobsModal] = useState(false);
|
||||||
const [analysisForm, setAnalysisForm] = useState({ symbol: '', date: '' });
|
const [analysisForm, setAnalysisForm] = useState({ symbol: '', date: '' });
|
||||||
const [isRunningAnalysis, setIsRunningAnalysis] = useState(false);
|
const [isRunningAnalysis, setIsRunningAnalysis] = useState(false);
|
||||||
const [selectedCompany, setSelectedCompany] = useState(null);
|
const [selectedCompany, setSelectedCompany] = useState(null);
|
||||||
|
|
@ -29,6 +30,11 @@ function App() {
|
||||||
const [transformedCompanyFiles, setTransformedCompanyFiles] = useState([]);
|
const [transformedCompanyFiles, setTransformedCompanyFiles] = useState([]);
|
||||||
const [activeDetailTab, setActiveDetailTab] = useState(null);
|
const [activeDetailTab, setActiveDetailTab] = useState(null);
|
||||||
|
|
||||||
|
// Job management state
|
||||||
|
const [jobs, setJobs] = useState([]);
|
||||||
|
const [jobsStats, setJobsStats] = useState({ total: 0, running: 0, completed: 0, failed: 0 });
|
||||||
|
const [isLoadingJobs, setIsLoadingJobs] = useState(false);
|
||||||
|
|
||||||
// Fields to display as pretty cards in the Details modal
|
// Fields to display as pretty cards in the Details modal
|
||||||
const detailFields = useMemo(() => ([
|
const detailFields = useMemo(() => ([
|
||||||
'market_report',
|
'market_report',
|
||||||
|
|
@ -106,6 +112,10 @@ function App() {
|
||||||
setShowResultsModal(false);
|
setShowResultsModal(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (showJobsModal) {
|
||||||
|
setShowJobsModal(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (showAnalysisModal) {
|
if (showAnalysisModal) {
|
||||||
setShowAnalysisModal(false);
|
setShowAnalysisModal(false);
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +123,7 @@ function App() {
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [showDetailModal, showWidgetsView, showTransformedDataModal, showResultsModal, showAnalysisModal]);
|
}, [showDetailModal, showWidgetsView, showTransformedDataModal, showResultsModal, showJobsModal, showAnalysisModal]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkBackendStatus();
|
checkBackendStatus();
|
||||||
|
|
@ -140,6 +150,10 @@ function App() {
|
||||||
setShowResultsModal(false);
|
setShowResultsModal(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (showJobsModal) {
|
||||||
|
setShowJobsModal(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (showAnalysisModal) {
|
if (showAnalysisModal) {
|
||||||
setShowAnalysisModal(false);
|
setShowAnalysisModal(false);
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +161,7 @@ function App() {
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [showDetailModal, showWidgetsView, showTransformedDataModal, showResultsModal, showAnalysisModal]);
|
}, [showDetailModal, showWidgetsView, showTransformedDataModal, showResultsModal, showJobsModal, showAnalysisModal]);
|
||||||
|
|
||||||
// Keep the active details tab in sync with available fields for the selected result
|
// Keep the active details tab in sync with available fields for the selected result
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -314,6 +328,7 @@ function App() {
|
||||||
setShowDetailModal(false);
|
setShowDetailModal(false);
|
||||||
setShowWidgetsView(false);
|
setShowWidgetsView(false);
|
||||||
setShowTransformedDataModal(false);
|
setShowTransformedDataModal(false);
|
||||||
|
setShowJobsModal(false);
|
||||||
setSelectedResult(null);
|
setSelectedResult(null);
|
||||||
setResultDetail(null);
|
setResultDetail(null);
|
||||||
setSelectedTransformedData(null);
|
setSelectedTransformedData(null);
|
||||||
|
|
@ -331,6 +346,32 @@ function App() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchJobs = async () => {
|
||||||
|
setIsLoadingJobs(true);
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/jobs');
|
||||||
|
const jobsData = response.data.jobs || [];
|
||||||
|
setJobs(jobsData);
|
||||||
|
const stats = {
|
||||||
|
total: jobsData.length,
|
||||||
|
running: jobsData.filter((job) => job.status === 'running').length,
|
||||||
|
completed: jobsData.filter((job) => job.status === 'completed').length,
|
||||||
|
failed: jobsData.filter((job) => job.status === 'failed').length,
|
||||||
|
};
|
||||||
|
setJobsStats(stats);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching jobs:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingJobs(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewJobResult = (job) => {
|
||||||
|
setSelectedResult(job);
|
||||||
|
setShowJobsModal(false);
|
||||||
|
setShowResultsModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
@ -421,7 +462,7 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<button
|
<button
|
||||||
onClick={handleStartAnalysis}
|
onClick={handleStartAnalysis}
|
||||||
className="group relative w-full flex justify-center py-8 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 ease-in-out"
|
className="group relative w-full flex justify-center py-8 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 ease-in-out"
|
||||||
|
|
@ -460,6 +501,19 @@ function App() {
|
||||||
<div className="text-sm opacity-90">View enhanced analyses</div>
|
<div className="text-sm opacity-90">View enhanced analyses</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowJobsModal(true)}
|
||||||
|
className="group relative w-full flex justify-center py-8 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition duration-150 ease-in-out"
|
||||||
|
>
|
||||||
|
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||||
|
<span className="text-2xl">📊</span>
|
||||||
|
</span>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-lg font-semibold">Job Management</div>
|
||||||
|
<div className="text-sm opacity-90">View and manage jobs</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -636,7 +690,7 @@ function App() {
|
||||||
<AnalysisDataAdapter tradingResult={resultDetail} />
|
<AnalysisDataAdapter tradingResult={resultDetail} />
|
||||||
) : (
|
) : (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
<div className="text-gray-500">Loading analysis data...</div>
|
<div className="text-gray-500 mb-2">Loading analysis data...</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -717,15 +771,25 @@ function App() {
|
||||||
<h3 className="text-lg font-medium text-gray-900">
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
{selectedCompany ? `${selectedCompany} Analysis Results` : "Analysis Results"}
|
{selectedCompany ? `${selectedCompany} Analysis Results` : "Analysis Results"}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
onClick={closeAllModals}
|
{selectedCompany && (
|
||||||
className="text-gray-400 hover:text-gray-600"
|
<button
|
||||||
>
|
onClick={() => { setSelectedCompany(null); setCompanyResults([]); }}
|
||||||
<span className="sr-only">Close</span>
|
className="text-sm px-3 py-1 rounded-md border text-gray-700 hover:bg-gray-50"
|
||||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
Back
|
||||||
</svg>
|
</button>
|
||||||
</button>
|
)}
|
||||||
|
<button
|
||||||
|
onClick={closeAllModals}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-h-96 overflow-y-auto">
|
<div className="max-h-96 overflow-y-auto">
|
||||||
|
|
@ -759,15 +823,6 @@ function App() {
|
||||||
// Company results view
|
// Company results view
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<button
|
|
||||||
onClick={() => {setSelectedCompany(null); setCompanyResults([]);}}
|
|
||||||
className="flex items-center text-indigo-600 hover:text-indigo-800 mr-4"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<h4 className="text-lg font-semibold">{selectedCompany} Results</h4>
|
<h4 className="text-lg font-semibold">{selectedCompany} Results</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -805,6 +860,154 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Jobs Modal */}
|
||||||
|
{showJobsModal && (
|
||||||
|
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||||
|
<div className="relative top-20 mx-auto p-5 border w-11/12 max-w-6xl shadow-lg rounded-md bg-white">
|
||||||
|
<div className="mt-3">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Job Management</h3>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={fetchJobs}
|
||||||
|
className="text-sm px-3 py-1 rounded-md border text-gray-700 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={closeAllModals}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Job Statistics */}
|
||||||
|
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||||
|
<div className="bg-blue-50 p-3 rounded-lg text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-600">{jobsStats.total}</div>
|
||||||
|
<div className="text-sm text-blue-800">Total Jobs</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-yellow-50 p-3 rounded-lg text-center">
|
||||||
|
<div className="text-2xl font-bold text-yellow-600">{jobsStats.running}</div>
|
||||||
|
<div className="text-sm text-yellow-800">Running</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 p-3 rounded-lg text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-600">{jobsStats.completed}</div>
|
||||||
|
<div className="text-sm text-green-800">Completed</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-red-50 p-3 rounded-lg text-center">
|
||||||
|
<div className="text-2xl font-bold text-red-600">{jobsStats.failed}</div>
|
||||||
|
<div className="text-sm text-red-800">Failed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoadingJobs ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<div className="text-gray-500 mb-2">Loading jobs...</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="max-h-96 overflow-y-auto">
|
||||||
|
{jobs.length > 0 ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{jobs.map((job, index) => (
|
||||||
|
<div key={job.job_id || index} className="bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
job.status === 'completed' ? 'bg-green-100 text-green-800' :
|
||||||
|
job.status === 'running' ? 'bg-yellow-100 text-yellow-800' :
|
||||||
|
job.status === 'failed' ? 'bg-red-100 text-red-800' :
|
||||||
|
'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{job.status === 'running' && '🔄'}
|
||||||
|
{job.status === 'completed' && '✅'}
|
||||||
|
{job.status === 'failed' && '❌'}
|
||||||
|
{job.status}
|
||||||
|
</span>
|
||||||
|
<div className="font-medium text-gray-900">
|
||||||
|
Job ID: {job.job_id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{job.result && (
|
||||||
|
<div className="text-sm text-gray-600 mb-1">
|
||||||
|
<span className="font-medium">Symbol:</span> {job.result.symbol} |
|
||||||
|
<span className="font-medium ml-2">Date:</span> {job.result.date}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{job.progress && (
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{job.progress}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{job.error && (
|
||||||
|
<div className="text-sm text-red-600 mt-1">
|
||||||
|
Error: {job.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{job.result && job.result.completed_at && (
|
||||||
|
<div className="text-xs text-gray-400 mt-1">
|
||||||
|
Completed: {new Date(job.result.completed_at).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{job.status === 'completed' && job.result && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => handleViewJobResult(job)}
|
||||||
|
className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||||
|
>
|
||||||
|
📈 View Results
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
const { symbol, date } = job.result;
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`http://localhost:8000/results/transformed/${symbol}/${date}`);
|
||||||
|
setSelectedTransformedData(response.data);
|
||||||
|
setShowJobsModal(false);
|
||||||
|
setShowTransformedDataModal(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching transformed result:', error);
|
||||||
|
// Fallback to regular result view
|
||||||
|
handleViewJobResult(job);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
🔄 Visualize
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<div className="text-gray-500 mb-2">No jobs found</div>
|
||||||
|
<div className="text-sm text-gray-400">Run a new analysis to see jobs here</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Detail Modal */}
|
{/* Detail Modal */}
|
||||||
{showDetailModal && (
|
{showDetailModal && (
|
||||||
<div className="fixed inset-0 bg-gray-900 bg-opacity-75 z-50">
|
<div className="fixed inset-0 bg-gray-900 bg-opacity-75 z-50">
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,23 @@ interface NewsItem {
|
||||||
interface NewsFeedWidgetProps {
|
interface NewsFeedWidgetProps {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
maxItems?: number;
|
maxItems?: number;
|
||||||
|
onBack?: () => void;
|
||||||
|
onRefresh?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NewsFeedWidget: React.FC<NewsFeedWidgetProps> = ({ symbol, maxItems = 10 }) => {
|
const NewsFeedWidget: React.FC<NewsFeedWidgetProps> = ({ symbol, maxItems = 10, onBack, onRefresh }) => {
|
||||||
const [newsItems, setNewsItems] = useState<NewsItem[]>([]);
|
const [newsItems, setNewsItems] = useState<NewsItem[]>([]);
|
||||||
const [filter, setFilter] = useState<'all' | 'macro' | 'company' | 'sector'>('all');
|
const [filter, setFilter] = useState<'all' | 'macro' | 'company' | 'sector'>('all');
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (onBack) return onBack();
|
||||||
|
if (typeof window !== 'undefined' && window.history) window.history.back();
|
||||||
|
};
|
||||||
|
const handleRefresh = () => {
|
||||||
|
if (onRefresh) return onRefresh();
|
||||||
|
if (typeof window !== 'undefined') window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
// Mock news data - in production, this would come from your backend
|
// Mock news data - in production, this would come from your backend
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mockNews: NewsItem[] = [
|
const mockNews: NewsItem[] = [
|
||||||
|
|
@ -103,8 +114,31 @@ const NewsFeedWidget: React.FC<NewsFeedWidgetProps> = ({ symbol, maxItems = 10 }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleBack}
|
||||||
|
className="p-2 rounded hover:bg-gray-100 text-gray-600"
|
||||||
|
aria-label="Back"
|
||||||
|
title="Back"
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</button>
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<h3 className="text-lg font-semibold">News Feed</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
className="p-2 rounded hover:bg-gray-100 text-gray-600"
|
||||||
|
aria-label="Refresh"
|
||||||
|
title="Refresh"
|
||||||
|
>
|
||||||
|
⟳
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="text-lg font-semibold">News Feed</h3>
|
<div className="flex-1" />
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
{['all', 'macro', 'company', 'sector'].map((filterOption) => (
|
{['all', 'macro', 'company', 'sector'].map((filterOption) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -120,6 +154,7 @@ const NewsFeedWidget: React.FC<NewsFeedWidgetProps> = ({ symbol, maxItems = 10 }
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,49 @@ interface AnalysisData {
|
||||||
interface AnalysisWidgetsProps {
|
interface AnalysisWidgetsProps {
|
||||||
data: AnalysisData;
|
data: AnalysisData;
|
||||||
rawData?: any;
|
rawData?: any;
|
||||||
|
onBackWidget?: () => void;
|
||||||
|
onRefreshWidget?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
|
const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBackWidget, onRefreshWidget }) => {
|
||||||
const [activeTab, setActiveTab] = useState('bull');
|
const [activeTab, setActiveTab] = useState('bull');
|
||||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
|
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
|
||||||
const [positionSize, setPositionSize] = useState(10000);
|
const [positionSize, setPositionSize] = useState(10000);
|
||||||
const [portfolioAllocation, setPortfolioAllocation] = useState(4);
|
const [portfolioAllocation, setPortfolioAllocation] = useState(4);
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (onBackWidget) return onBackWidget();
|
||||||
|
if (typeof window !== 'undefined' && window.history) window.history.back();
|
||||||
|
};
|
||||||
|
const handleRefresh = () => {
|
||||||
|
if (onRefreshWidget) return onRefreshWidget();
|
||||||
|
if (typeof window !== 'undefined') window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
const WidgetHeader: React.FC<{ title: string }> = ({ title }) => (
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleBack}
|
||||||
|
className="p-2 rounded hover:bg-gray-100 text-gray-600"
|
||||||
|
aria-label="Back"
|
||||||
|
title="Back"
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</button>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
className="p-2 rounded hover:bg-gray-100 text-gray-600"
|
||||||
|
aria-label="Refresh"
|
||||||
|
title="Refresh"
|
||||||
|
>
|
||||||
|
⟳
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
// Mock price data for chart
|
// Mock price data for chart
|
||||||
const priceData = [
|
const priceData = [
|
||||||
{ date: '2024-07-01', price: 4.20, volume: 1200000 },
|
{ date: '2024-07-01', price: 4.20, volume: 1200000 },
|
||||||
|
|
@ -107,7 +142,7 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||||
{/* Stock Price Chart */}
|
{/* Stock Price Chart */}
|
||||||
<div className="lg:col-span-2 bg-white rounded-lg shadow-md p-6">
|
<div className="lg:col-span-2 bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Stock Price Chart</h3>
|
<WidgetHeader title="Stock Price Chart" />
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
<LineChart data={priceData}>
|
<LineChart data={priceData}>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
|
@ -131,7 +166,7 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
|
||||||
|
|
||||||
{/* Technical Indicators Dashboard */}
|
{/* Technical Indicators Dashboard */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Technical Indicators</h3>
|
<WidgetHeader title="Technical Indicators" />
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-gray-600">RSI</span>
|
<span className="text-gray-600">RSI</span>
|
||||||
|
|
@ -186,7 +221,7 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||||
{/* Bull vs Bear Debate Viewer */}
|
{/* Bull vs Bear Debate Viewer */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Bull vs Bear Debate</h3>
|
<WidgetHeader title="Bull vs Bear Debate" />
|
||||||
<div className="flex space-x-1 mb-4">
|
<div className="flex space-x-1 mb-4">
|
||||||
<button
|
<button
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium ${
|
className={`px-4 py-2 rounded-lg text-sm font-medium ${
|
||||||
|
|
@ -242,7 +277,7 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
|
||||||
|
|
||||||
{/* Investment Plan Timeline */}
|
{/* Investment Plan Timeline */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Investment Plan Timeline</h3>
|
<WidgetHeader title="Investment Plan Timeline" />
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="w-4 h-4 bg-blue-500 rounded-full mr-4"></div>
|
<div className="w-4 h-4 bg-blue-500 rounded-full mr-4"></div>
|
||||||
|
|
@ -280,7 +315,7 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||||
{/* Risk Assessment Gauge */}
|
{/* Risk Assessment Gauge */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Risk Assessment</h3>
|
<WidgetHeader title="Risk Assessment" />
|
||||||
<div className="flex items-center justify-center mb-4">
|
<div className="flex items-center justify-center mb-4">
|
||||||
<div className="relative w-32 h-32">
|
<div className="relative w-32 h-32">
|
||||||
<svg className="w-32 h-32 transform -rotate-90" viewBox="0 0 36 36">
|
<svg className="w-32 h-32 transform -rotate-90" viewBox="0 0 36 36">
|
||||||
|
|
@ -314,7 +349,7 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
|
||||||
|
|
||||||
{/* Position Calculator */}
|
{/* Position Calculator */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Position Calculator</h3>
|
<WidgetHeader title="Position Calculator" />
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
|
@ -350,7 +385,7 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
|
||||||
|
|
||||||
{/* Sentiment Thermometer */}
|
{/* Sentiment Thermometer */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Sentiment Thermometer</h3>
|
<WidgetHeader title="Sentiment Thermometer" />
|
||||||
<div className="flex items-center justify-center mb-4">
|
<div className="flex items-center justify-center mb-4">
|
||||||
<div className="w-8 h-32 bg-gray-200 rounded-full relative">
|
<div className="w-8 h-32 bg-gray-200 rounded-full relative">
|
||||||
<div
|
<div
|
||||||
|
|
@ -376,12 +411,12 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||||
{/* News Feed Widget */}
|
{/* News Feed Widget */}
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<NewsFeedWidget symbol={data.symbol} maxItems={8} />
|
<NewsFeedWidget symbol={data.symbol} maxItems={8} onBack={handleBack} onRefresh={handleRefresh} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Earnings Countdown Timer */}
|
{/* Earnings Countdown Timer */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Earnings Countdown</h3>
|
<WidgetHeader title="Earnings Countdown" />
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-3xl font-bold text-blue-600 mb-2">{data.earningsDate}</p>
|
<p className="text-3xl font-bold text-blue-600 mb-2">{data.earningsDate}</p>
|
||||||
<p className="text-sm text-gray-600 mb-4">Next Earnings Call</p>
|
<p className="text-sm text-gray-600 mb-4">Next Earnings Call</p>
|
||||||
|
|
@ -399,7 +434,7 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
|
||||||
|
|
||||||
{/* Ownership Structure Pie Chart */}
|
{/* Ownership Structure Pie Chart */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Ownership Structure</h3>
|
<WidgetHeader title="Ownership Structure" />
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<Pie
|
<Pie
|
||||||
|
|
@ -422,7 +457,7 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
|
||||||
|
|
||||||
{/* Decision History Tracker */}
|
{/* Decision History Tracker */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
|
||||||
<h3 className="text-lg font-semibold mb-4">Decision History Tracker</h3>
|
<WidgetHeader title="Decision History Tracker" />
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute left-4 top-0 bottom-0 w-0.5 bg-gray-300"></div>
|
<div className="absolute left-4 top-0 bottom-0 w-0.5 bg-gray-300"></div>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
@ -460,7 +495,7 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Executive Summary Box */}
|
{/* Executive Summary Box */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Executive Summary</h3>
|
<WidgetHeader title="Executive Summary" />
|
||||||
<div className="bg-blue-50 border-l-4 border-blue-400 p-4">
|
<div className="bg-blue-50 border-l-4 border-blue-400 p-4">
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
<strong>Final Recommendation: {data.finalDecision}</strong> - {data.decisionReasoning}
|
<strong>Final Recommendation: {data.finalDecision}</strong> - {data.decisionReasoning}
|
||||||
|
|
@ -500,7 +535,7 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
|
||||||
{/* Raw Data Viewer */}
|
{/* Raw Data Viewer */}
|
||||||
{rawData && (
|
{rawData && (
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Detailed Analysis Data</h3>
|
<WidgetHeader title="Detailed Analysis Data" />
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{Object.entries(rawData).map(([key, value]) => (
|
{Object.entries(rawData).map(([key, value]) => (
|
||||||
<div key={key} className="space-y-2">
|
<div key={key} className="space-y-2">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue