1082 lines
49 KiB
JavaScript
1082 lines
49 KiB
JavaScript
import React, { useState, useEffect, useMemo } from 'react';
|
|
import axios from 'axios';
|
|
import AnalysisDataAdapter from './components/AnalysisDataAdapter.tsx';
|
|
import TransformedDataAdapter from './components/TransformedDataAdapter.tsx';
|
|
import transformedDataService from './services/transformedDataService.ts';
|
|
|
|
function App() {
|
|
const [backendStatus, setBackendStatus] = useState('checking');
|
|
const [companies, setCompanies] = useState([]);
|
|
const [stats, setStats] = useState({ totalAnalyses: 0, companies: 0 });
|
|
const [showAnalysisModal, setShowAnalysisModal] = useState(false);
|
|
const [showResultsModal, setShowResultsModal] = useState(false);
|
|
const [showDetailModal, setShowDetailModal] = useState(false);
|
|
const [showWidgetsView, setShowWidgetsView] = useState(false);
|
|
const [showTransformedDataModal, setShowTransformedDataModal] = useState(false);
|
|
const [showJobsModal, setShowJobsModal] = useState(false);
|
|
const [analysisForm, setAnalysisForm] = useState({ symbol: '', date: '' });
|
|
const [isRunningAnalysis, setIsRunningAnalysis] = useState(false);
|
|
const [selectedCompany, setSelectedCompany] = useState(null);
|
|
const [companyResults, setCompanyResults] = useState([]);
|
|
const [selectedResult, setSelectedResult] = useState(null);
|
|
const [resultDetail, setResultDetail] = useState(null);
|
|
|
|
// New state for transformed data
|
|
const [transformedDataSummary, setTransformedDataSummary] = useState(null);
|
|
const [selectedTransformedData, setSelectedTransformedData] = useState(null);
|
|
const [isLoadingTransformedData, setIsLoadingTransformedData] = useState(false);
|
|
const [transformedDataError, setTransformedDataError] = useState(null);
|
|
const [selectedTransformedCompany, setSelectedTransformedCompany] = useState(null);
|
|
const [transformedCompanyFiles, setTransformedCompanyFiles] = useState([]);
|
|
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
|
|
const detailFields = useMemo(() => ([
|
|
'market_report',
|
|
'sentiment_report',
|
|
'news_report',
|
|
'fundamentals_report',
|
|
'trader_investment_decision',
|
|
'investment_plan',
|
|
'final_trade_decision',
|
|
'investment_debate_state.bull_history',
|
|
'investment_debate_state.bear_history',
|
|
'investment_debate_state.history',
|
|
'investment_debate_state.current_response',
|
|
'investment_debate_state.judge_decision',
|
|
'risk_debate_state.risky_history',
|
|
'risk_debate_state.safe_history',
|
|
'risk_debate_state.neutral_history',
|
|
'risk_debate_state.history',
|
|
'risk_debate_state.judge_decision',
|
|
'company_of_interest',
|
|
'trade_date',
|
|
]), []);
|
|
|
|
const fieldLabelMap = {
|
|
market_report: 'Market Report',
|
|
sentiment_report: 'Sentiment Report',
|
|
news_report: 'News Report',
|
|
fundamentals_report: 'Fundamentals Report',
|
|
trader_investment_decision: 'Trader Investment Decision',
|
|
investment_plan: 'Investment Plan',
|
|
final_trade_decision: 'Final Trade Decision',
|
|
'investment_debate_state.bull_history': 'Investment Debate - Bull History',
|
|
'investment_debate_state.bear_history': 'Investment Debate - Bear History',
|
|
'investment_debate_state.history': 'Investment Debate - History',
|
|
'investment_debate_state.current_response': 'Investment Debate - Current Response',
|
|
'investment_debate_state.judge_decision': 'Investment Debate - Judge Decision',
|
|
'risk_debate_state.risky_history': 'Risk Debate - Risky History',
|
|
'risk_debate_state.safe_history': 'Risk Debate - Safe History',
|
|
'risk_debate_state.neutral_history': 'Risk Debate - Neutral History',
|
|
'risk_debate_state.history': 'Risk Debate - History',
|
|
'risk_debate_state.judge_decision': 'Risk Debate - Judge Decision',
|
|
company_of_interest: 'Company of Interest',
|
|
trade_date: 'Trade Date',
|
|
};
|
|
|
|
// Helpers
|
|
const getNested = (obj, path) => {
|
|
if (!obj || !path) return undefined;
|
|
return path.split('.').reduce((acc, key) => (acc != null ? acc[key] : undefined), obj);
|
|
};
|
|
|
|
const prettyValue = (val) => {
|
|
if (val === null || val === undefined) return '';
|
|
if (typeof val === 'string') return val;
|
|
try { return JSON.stringify(val, null, 2); } catch { return String(val); }
|
|
};
|
|
|
|
// Close only the topmost open modal on Escape, preserving underlying modals
|
|
useEffect(() => {
|
|
const handleKeyDown = (e) => {
|
|
if (e.key === 'Escape') {
|
|
if (showDetailModal) {
|
|
setShowDetailModal(false);
|
|
return;
|
|
}
|
|
if (showWidgetsView) {
|
|
setShowWidgetsView(false);
|
|
return;
|
|
}
|
|
if (showTransformedDataModal) {
|
|
setShowTransformedDataModal(false);
|
|
return;
|
|
}
|
|
if (showResultsModal) {
|
|
setShowResultsModal(false);
|
|
return;
|
|
}
|
|
if (showJobsModal) {
|
|
setShowJobsModal(false);
|
|
return;
|
|
}
|
|
if (showAnalysisModal) {
|
|
setShowAnalysisModal(false);
|
|
}
|
|
}
|
|
};
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [showDetailModal, showWidgetsView, showTransformedDataModal, showResultsModal, showJobsModal, showAnalysisModal]);
|
|
|
|
useEffect(() => {
|
|
checkBackendStatus();
|
|
fetchCompanies();
|
|
loadTransformedDataSummary();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (e) => {
|
|
if (e.key === 'Escape') {
|
|
if (showDetailModal) {
|
|
setShowDetailModal(false);
|
|
return;
|
|
}
|
|
if (showWidgetsView) {
|
|
setShowWidgetsView(false);
|
|
return;
|
|
}
|
|
if (showTransformedDataModal) {
|
|
setShowTransformedDataModal(false);
|
|
return;
|
|
}
|
|
if (showResultsModal) {
|
|
setShowResultsModal(false);
|
|
return;
|
|
}
|
|
if (showJobsModal) {
|
|
setShowJobsModal(false);
|
|
return;
|
|
}
|
|
if (showAnalysisModal) {
|
|
setShowAnalysisModal(false);
|
|
}
|
|
}
|
|
};
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [showDetailModal, showWidgetsView, showTransformedDataModal, showResultsModal, showJobsModal, showAnalysisModal]);
|
|
|
|
// Keep the active details tab in sync with available fields for the selected result
|
|
useEffect(() => {
|
|
const baseData = resultDetail?.data?.[selectedResult?.date];
|
|
if (!baseData) {
|
|
setActiveDetailTab(null);
|
|
return;
|
|
}
|
|
const available = (detailFields || []).filter((path) => {
|
|
const val = path.split('.').reduce((acc, k) => (acc != null ? acc[k] : undefined), baseData);
|
|
if (val === undefined || val === null) return false;
|
|
if (typeof val === 'object') {
|
|
if (Array.isArray(val)) return val.length > 0;
|
|
return Object.keys(val).length > 0;
|
|
}
|
|
return true;
|
|
});
|
|
if (available.length === 0) {
|
|
setActiveDetailTab(null);
|
|
} else if (!activeDetailTab || !available.includes(activeDetailTab)) {
|
|
setActiveDetailTab(available[0]);
|
|
}
|
|
}, [resultDetail, selectedResult, detailFields, activeDetailTab]);
|
|
|
|
const checkBackendStatus = async () => {
|
|
try {
|
|
await axios.get('/health');
|
|
setBackendStatus('connected');
|
|
} catch (error) {
|
|
setBackendStatus('disconnected');
|
|
}
|
|
};
|
|
|
|
const fetchCompanies = async () => {
|
|
try {
|
|
const response = await axios.get('/results/companies');
|
|
const companiesData = response.data.companies || [];
|
|
setCompanies(companiesData);
|
|
|
|
const totalAnalyses = companiesData.reduce((sum, company) => sum + company.total_analyses, 0);
|
|
setStats({
|
|
totalAnalyses,
|
|
companies: companiesData.length
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching companies:', error);
|
|
}
|
|
};
|
|
|
|
const loadTransformedDataSummary = async () => {
|
|
try {
|
|
const summary = await transformedDataService.getDataSummary();
|
|
setTransformedDataSummary(summary);
|
|
} catch (error) {
|
|
console.error('Error loading transformed data summary:', error);
|
|
setTransformedDataError('Failed to load transformed data');
|
|
}
|
|
};
|
|
|
|
const fetchCompanyResults = async (symbol) => {
|
|
try {
|
|
const response = await axios.get(`/results/${symbol}`);
|
|
setCompanyResults(response.data.results || []);
|
|
setSelectedCompany(symbol);
|
|
} catch (error) {
|
|
console.error('Error fetching company results:', error);
|
|
alert('Error loading company results');
|
|
}
|
|
};
|
|
|
|
const openDetailModal = async (result) => {
|
|
try {
|
|
const response = await axios.get(`/results/${selectedCompany}/${result.date}`);
|
|
setResultDetail(response.data);
|
|
setSelectedResult({ ...result, company: selectedCompany });
|
|
setShowDetailModal(true);
|
|
} catch (error) {
|
|
console.error('Error fetching result detail:', error);
|
|
alert('Error loading result details');
|
|
}
|
|
};
|
|
|
|
const openWidgetsView = async (result) => {
|
|
try {
|
|
const response = await axios.get(`/results/${selectedCompany}/${result.date}`);
|
|
setResultDetail(response.data);
|
|
setSelectedResult({ ...result, company: selectedCompany });
|
|
setShowWidgetsView(true);
|
|
} catch (error) {
|
|
console.error('Error fetching result detail:', error);
|
|
alert('Error loading analysis dashboard');
|
|
}
|
|
};
|
|
|
|
const openTransformedWidgetsView = async (file) => {
|
|
setIsLoadingTransformedData(true);
|
|
setTransformedDataError(null);
|
|
|
|
try {
|
|
// Load by company and date to avoid relying on cached index
|
|
const transformedData = await transformedDataService.loadByCompanyAndDate(selectedTransformedCompany, file.date);
|
|
setSelectedTransformedData(transformedData);
|
|
setShowWidgetsView(true);
|
|
} catch (error) {
|
|
console.error('Error loading transformed data:', error);
|
|
setTransformedDataError(`Failed to load ${file.filename}: ${error.message}`);
|
|
} finally {
|
|
setIsLoadingTransformedData(false);
|
|
}
|
|
};
|
|
|
|
const handleStartAnalysis = () => {
|
|
if (backendStatus !== 'connected') {
|
|
alert('Backend is not connected. Please ensure the backend server is running.');
|
|
return;
|
|
}
|
|
setShowAnalysisModal(true);
|
|
};
|
|
|
|
const handleViewResults = () => {
|
|
if (backendStatus !== 'connected') {
|
|
alert('Backend is not connected. Please ensure the backend server is running.');
|
|
return;
|
|
}
|
|
setShowResultsModal(true);
|
|
setSelectedCompany(null);
|
|
setCompanyResults([]);
|
|
};
|
|
|
|
const handleViewTransformedData = async () => {
|
|
setShowTransformedDataModal(true);
|
|
setTransformedDataError(null);
|
|
setSelectedTransformedCompany(null);
|
|
setTransformedCompanyFiles([]);
|
|
try {
|
|
transformedDataService.clearCache();
|
|
const summary = await transformedDataService.getDataSummary();
|
|
setTransformedDataSummary(summary);
|
|
} catch (e) {
|
|
console.error('Failed to load transformed data on open:', e);
|
|
setTransformedDataError('Failed to load transformed data');
|
|
}
|
|
};
|
|
|
|
const runAnalysis = async () => {
|
|
setIsRunningAnalysis(true);
|
|
try {
|
|
await axios.post('/analysis/start', analysisForm);
|
|
alert('Analysis completed successfully!');
|
|
setShowAnalysisModal(false);
|
|
setAnalysisForm({ symbol: '', date: '' });
|
|
fetchCompanies(); // Refresh the companies list
|
|
} catch (error) {
|
|
console.error('Error running analysis:', error);
|
|
alert('Error running analysis. Please try again.');
|
|
} finally {
|
|
setIsRunningAnalysis(false);
|
|
}
|
|
};
|
|
|
|
const closeAllModals = () => {
|
|
setShowAnalysisModal(false);
|
|
setShowResultsModal(false);
|
|
setShowDetailModal(false);
|
|
setShowWidgetsView(false);
|
|
setShowTransformedDataModal(false);
|
|
setShowJobsModal(false);
|
|
setSelectedResult(null);
|
|
setResultDetail(null);
|
|
setSelectedTransformedData(null);
|
|
setTransformedDataError(null);
|
|
};
|
|
|
|
const fetchTransformedCompanyFiles = async (symbol) => {
|
|
try {
|
|
const response = await axios.get(`/transformed-results/${symbol}`);
|
|
setTransformedCompanyFiles(response.data.results || []);
|
|
setSelectedTransformedCompany(symbol);
|
|
} catch (error) {
|
|
console.error('Error fetching transformed company files:', error);
|
|
setTransformedDataError('Error loading transformed company files');
|
|
}
|
|
};
|
|
|
|
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 (
|
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
|
{/* Header */}
|
|
<div className="bg-white shadow-lg">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between items-center py-6">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<h1 className="text-3xl font-bold text-gray-900">TradingAgents</h1>
|
|
</div>
|
|
<div className="ml-4">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
backendStatus === 'connected'
|
|
? 'bg-green-100 text-green-800'
|
|
: backendStatus === 'disconnected'
|
|
? 'bg-red-100 text-red-800'
|
|
: 'bg-yellow-100 text-yellow-800'
|
|
}`}>
|
|
{backendStatus === 'connected' ? '● Connected' :
|
|
backendStatus === 'disconnected' ? '● Disconnected' : '● Checking...'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
|
<div className="px-4 py-6 sm:px-0">
|
|
{/* Stats Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
<div className="bg-white overflow-hidden shadow rounded-lg">
|
|
<div className="p-5">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
|
|
<span className="text-white font-semibold">📊</span>
|
|
</div>
|
|
</div>
|
|
<div className="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt className="text-sm font-medium text-gray-500 truncate">Total Analyses</dt>
|
|
<dd className="text-lg font-medium text-gray-900">{stats.totalAnalyses}</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white overflow-hidden shadow rounded-lg">
|
|
<div className="p-5">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
|
|
<span className="text-white font-semibold">🏢</span>
|
|
</div>
|
|
</div>
|
|
<div className="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt className="text-sm font-medium text-gray-500 truncate">Companies</dt>
|
|
<dd className="text-lg font-medium text-gray-900">{stats.companies}</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white overflow-hidden shadow rounded-lg">
|
|
<div className="p-5">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0">
|
|
<div className="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
|
|
<span className="text-white font-semibold">🔄</span>
|
|
</div>
|
|
</div>
|
|
<div className="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt className="text-sm font-medium text-gray-500 truncate">Transformed Data</dt>
|
|
<dd className="text-lg font-medium text-gray-900">
|
|
{transformedDataSummary ? transformedDataSummary.totalFiles : '---'}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<button
|
|
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"
|
|
>
|
|
<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">Run New Analysis</div>
|
|
<div className="text-sm opacity-90">Execute TradingAgents pipeline</div>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleViewResults}
|
|
className="group relative w-full flex justify-center py-8 px-4 border border-transparent text-sm 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 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">View Agent Outputs</div>
|
|
<div className="text-sm opacity-90">Browse analysis results</div>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleViewTransformedData}
|
|
className="group relative w-full flex justify-center py-8 px-4 border border-transparent text-sm 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 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">Visualize Output Data</div>
|
|
<div className="text-sm opacity-90">View enhanced analyses</div>
|
|
</div>
|
|
</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>
|
|
|
|
{/* Transformed Data Modal */}
|
|
{showTransformedDataModal && (
|
|
<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-4xl 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">
|
|
{selectedTransformedCompany ? `${selectedTransformedCompany} Transformed Analyses` : 'Transformed Analysis Data'}
|
|
</h3>
|
|
<div className="flex items-center gap-2">
|
|
{selectedTransformedCompany && (
|
|
<button
|
|
onClick={() => {
|
|
setSelectedTransformedCompany(null);
|
|
setTransformedCompanyFiles([]);
|
|
}}
|
|
className="text-sm px-3 py-1 rounded-md border text-gray-700 hover:bg-gray-50"
|
|
>
|
|
Back
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={async () => {
|
|
try {
|
|
transformedDataService.clearCache();
|
|
const summary = await transformedDataService.getDataSummary();
|
|
setTransformedDataSummary(summary);
|
|
setTransformedDataError(null);
|
|
} catch (e) {
|
|
console.error('Refresh failed:', e);
|
|
setTransformedDataError('Refresh failed');
|
|
}
|
|
}}
|
|
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>
|
|
|
|
{transformedDataError && (
|
|
<div className="mb-4 bg-red-50 border border-red-200 rounded-md p-4">
|
|
<div className="flex">
|
|
<div className="flex-shrink-0">
|
|
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<div className="ml-3">
|
|
<p className="text-sm text-red-800">{transformedDataError}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{transformedDataSummary && (
|
|
<div className="mb-6 bg-blue-50 border border-blue-200 rounded-md p-4">
|
|
<h4 className="text-sm font-medium text-blue-900 mb-2">Data Summary</h4>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-blue-600 font-medium">Total Files:</span>
|
|
<span className="ml-1 text-blue-900">{transformedDataSummary.totalFiles}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-blue-600 font-medium">Companies:</span>
|
|
<span className="ml-1 text-blue-900">{transformedDataSummary.companies.join(', ')}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-blue-600 font-medium">Date Range:</span>
|
|
<span className="ml-1 text-blue-900">
|
|
{transformedDataSummary.dateRange.earliest} to {transformedDataSummary.dateRange.latest}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="max-h-96 overflow-y-auto">
|
|
{!selectedTransformedCompany ? (
|
|
// Company list view (only companies with transformed analyses)
|
|
companies && companies.filter(c => (c.transformed_analyses || 0) > 0).length > 0 ? (
|
|
<div className="space-y-2">
|
|
{companies.filter(c => (c.transformed_analyses || 0) > 0).map((company) => (
|
|
<div
|
|
key={company.symbol}
|
|
className="flex items-center justify-between p-3 bg-gray-50 rounded-md hover:bg-gray-100 cursor-pointer"
|
|
onClick={() => fetchTransformedCompanyFiles(company.symbol)}
|
|
>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{company.symbol}</div>
|
|
<div className="text-sm text-gray-500">Transformed analyses: {company.transformed_analyses}</div>
|
|
</div>
|
|
<div className="text-sm text-gray-400">
|
|
View Files
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<div className="text-gray-500 mb-2">No companies with transformed data found</div>
|
|
<div className="text-sm text-gray-400">Run the data transformation agent to generate transformed analyses</div>
|
|
</div>
|
|
)
|
|
) : (
|
|
// File list for selected transformed company
|
|
transformedCompanyFiles.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{transformedCompanyFiles.map((file, index) => (
|
|
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-md hover:bg-gray-100">
|
|
<div>
|
|
<div className="font-medium text-gray-900">{selectedTransformedCompany} - {file.date}</div>
|
|
<div className="text-sm text-gray-500">{file.filename}</div>
|
|
</div>
|
|
<button
|
|
onClick={() => openTransformedWidgetsView(file)}
|
|
disabled={isLoadingTransformedData}
|
|
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 disabled:opacity-50"
|
|
>
|
|
{isLoadingTransformedData ? 'Loading...' : 'View Dashboard'}
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<div className="text-gray-500 mb-2">No transformed files found for {selectedTransformedCompany}</div>
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Widgets View Modal */}
|
|
{showWidgetsView && (
|
|
<div className="fixed inset-0 bg-gray-900 bg-opacity-75 z-50">
|
|
<div className="min-h-screen flex items-center justify-center p-4">
|
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-7xl max-h-screen overflow-hidden">
|
|
<div className="flex justify-between items-center p-4 border-b">
|
|
<h2 className="text-xl font-semibold text-gray-900">
|
|
Analysis Dashboard
|
|
{selectedResult && ` - ${selectedResult.company} (${selectedResult.date})`}
|
|
{selectedTransformedData && ` - ${selectedTransformedData.metadata.company_ticker} (${selectedTransformedData.metadata.analysis_date})`}
|
|
</h2>
|
|
<button
|
|
onClick={closeAllModals}
|
|
className="text-gray-400 hover:text-gray-600"
|
|
>
|
|
<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 className="overflow-y-auto max-h-[calc(100vh-100px)]">
|
|
{selectedTransformedData ? (
|
|
<TransformedDataAdapter analysisData={selectedTransformedData} />
|
|
) : resultDetail ? (
|
|
<AnalysisDataAdapter tradingResult={resultDetail} />
|
|
) : (
|
|
<div className="p-8 text-center">
|
|
<div className="text-gray-500 mb-2">Loading analysis data...</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Analysis Modal */}
|
|
{showAnalysisModal && (
|
|
<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-96 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">Start New Analysis</h3>
|
|
<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 className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Stock Symbol
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={analysisForm.symbol}
|
|
onChange={(e) => setAnalysisForm({...analysisForm, symbol: e.target.value})}
|
|
placeholder="e.g., AAPL, TSLA, NVDA"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Analysis Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={analysisForm.date}
|
|
onChange={(e) => setAnalysisForm({...analysisForm, date: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
/>
|
|
</div>
|
|
<div className="flex space-x-3 pt-4">
|
|
<button
|
|
onClick={runAnalysis}
|
|
disabled={isRunningAnalysis}
|
|
className="flex-1 bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{isRunningAnalysis ? 'Starting...' : 'Start Analysis'}
|
|
</button>
|
|
<button
|
|
onClick={closeAllModals}
|
|
className="flex-1 bg-gray-300 text-gray-700 py-2 px-4 rounded-md hover:bg-gray-400"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Results Modal */}
|
|
{showResultsModal && (
|
|
<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">
|
|
{selectedCompany ? `${selectedCompany} Analysis Results` : "Analysis Results"}
|
|
</h3>
|
|
<div className="flex items-center gap-2">
|
|
{selectedCompany && (
|
|
<button
|
|
onClick={() => { setSelectedCompany(null); setCompanyResults([]); }}
|
|
className="text-sm px-3 py-1 rounded-md border text-gray-700 hover:bg-gray-50"
|
|
>
|
|
Back
|
|
</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 className="max-h-96 overflow-y-auto">
|
|
{!selectedCompany ? (
|
|
// Company list view
|
|
companies.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{companies.map((company) => (
|
|
<div
|
|
key={company.symbol}
|
|
className="flex items-center justify-between p-3 bg-gray-50 rounded-md hover:bg-gray-100 cursor-pointer"
|
|
onClick={() => fetchCompanyResults(company.symbol)}
|
|
>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{company.symbol}</div>
|
|
<div className="text-sm text-gray-500">{company.total_analyses} analyses</div>
|
|
</div>
|
|
<div className="text-sm text-gray-400">
|
|
{company.latest_analysis}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<div className="text-gray-500 mb-2">No analysis results available</div>
|
|
<div className="text-sm text-gray-400">Start your first analysis to see results here</div>
|
|
</div>
|
|
)
|
|
) : (
|
|
// Company results view
|
|
<div>
|
|
<div className="flex items-center mb-4">
|
|
<h4 className="text-lg font-semibold">{selectedCompany} Results</h4>
|
|
</div>
|
|
|
|
{companyResults.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{companyResults.map((result, index) => (
|
|
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-md hover:bg-gray-100">
|
|
<div>
|
|
<div className="font-medium text-gray-900">{result.filename}</div>
|
|
<div className="text-sm text-gray-500">
|
|
{new Date(result.timestamp).toLocaleString()}
|
|
</div>
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
<button
|
|
onClick={() => openDetailModal(result)}
|
|
className="inline-flex items-center px-3 py-1 border border-gray-300 text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
>
|
|
View Details
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<div className="text-gray-500">No results found for {selectedCompany}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</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 */}
|
|
{showDetailModal && (
|
|
<div className="fixed inset-0 bg-gray-900 bg-opacity-75 z-50">
|
|
<div className="min-h-screen flex items-center justify-center p-4">
|
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-screen overflow-hidden">
|
|
<div className="flex justify-between items-center p-4 border-b">
|
|
<h2 className="text-xl font-semibold text-gray-900">
|
|
{selectedResult?.company} Analysis Details
|
|
</h2>
|
|
<button
|
|
onClick={closeAllModals}
|
|
className="text-gray-400 hover:text-gray-600"
|
|
>
|
|
<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 className="overflow-y-auto max-h-[calc(100vh-100px)] p-6">
|
|
{/* Dropdown selector */}
|
|
{(() => {
|
|
const baseData = resultDetail?.data?.[selectedResult?.date];
|
|
if (!baseData) return null;
|
|
const available = detailFields
|
|
.map((path) => ({ path, val: getNested(baseData, path) }))
|
|
.filter(({ val }) => {
|
|
if (val === undefined || val === null) return false;
|
|
if (typeof val === 'object') {
|
|
if (Array.isArray(val)) return val.length > 0;
|
|
return Object.keys(val).length > 0;
|
|
}
|
|
return true;
|
|
});
|
|
if (available.length === 0) return null;
|
|
const current = available.find(({ path }) => path === activeDetailTab) || available[0];
|
|
const activePath = current.path;
|
|
const activeVal = current.val;
|
|
return (
|
|
<div className="mb-6">
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Select section</label>
|
|
<select
|
|
className="block w-full md:max-w-sm border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm"
|
|
value={activePath}
|
|
onChange={(e) => setActiveDetailTab(e.target.value)}
|
|
>
|
|
{available.map(({ path }) => (
|
|
<option key={path} value={path}>
|
|
{fieldLabelMap[path] || path}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
{/* Content */}
|
|
<div className="bg-white border rounded-lg p-4 shadow-sm">
|
|
<h4 className="font-semibold mb-2 text-gray-900">{fieldLabelMap[activePath] || activePath}</h4>
|
|
<pre className="text-sm text-gray-800 whitespace-pre-wrap overflow-x-auto">{prettyValue(activeVal)}</pre>
|
|
</div>
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|