add
This commit is contained in:
parent
bdc27679fa
commit
a556099d97
Binary file not shown.
|
|
@ -2,7 +2,7 @@ import { useState, useMemo, useEffect, useCallback } from 'react';
|
|||
import { Link } from 'react-router-dom';
|
||||
import { Calendar, TrendingUp, TrendingDown, Minus, ChevronRight, BarChart3, Target, HelpCircle, Activity, Calculator, LineChart, PieChart, Shield, Filter, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { sampleRecommendations, getBacktestResult as getStaticBacktestResult, calculateAccuracyMetrics as calculateStaticAccuracyMetrics, getDateStats as getStaticDateStats, getOverallStats as getStaticOverallStats, getReturnBreakdown as getStaticReturnBreakdown } from '../data/recommendations';
|
||||
import { DecisionBadge } from '../components/StockCard';
|
||||
import { DecisionBadge, HoldDaysBadge } from '../components/StockCard';
|
||||
import Sparkline from '../components/Sparkline';
|
||||
import AccuracyBadge from '../components/AccuracyBadge';
|
||||
import AccuracyExplainModal from '../components/AccuracyExplainModal';
|
||||
|
|
@ -1158,6 +1158,7 @@ export default function History() {
|
|||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<DecisionBadge decision={stock.decision} size="small" />
|
||||
<HoldDaysBadge holdDays={stock.hold_days} decision={stock.decision} />
|
||||
{predictionCorrect !== null && (
|
||||
<AccuracyBadge
|
||||
correct={predictionCorrect}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,46 @@
|
|||
import { useParams, Link } from 'react-router-dom';
|
||||
import { useMemo, useState, useEffect } from 'react';
|
||||
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
ArrowLeft, Building2, TrendingUp, TrendingDown, Minus, AlertTriangle,
|
||||
Calendar, Activity, LineChart, Database, MessageSquare, FileText, Layers,
|
||||
RefreshCw, Play, Loader2
|
||||
Calendar, Activity, LineChart, Database, MessageSquare, Layers,
|
||||
RefreshCw, Play, Loader2, CheckCircle, XCircle, Target, BarChart3, Square
|
||||
} from 'lucide-react';
|
||||
import { NIFTY_50_STOCKS } from '../types';
|
||||
import { sampleRecommendations, getStockHistory, getExtendedPriceHistory, getPredictionPointsWithPrices, getRawAnalysis } from '../data/recommendations';
|
||||
import { DecisionBadge, ConfidenceBadge, RiskBadge } from '../components/StockCard';
|
||||
import type { DailyRecommendation, StockAnalysis } from '../types';
|
||||
import { sampleRecommendations, getStockHistory as getStaticStockHistory, getRawAnalysis } from '../data/recommendations';
|
||||
import { DecisionBadge, ConfidenceBadge, RiskBadge, HoldDaysBadge } from '../components/StockCard';
|
||||
import AIAnalysisPanel from '../components/AIAnalysisPanel';
|
||||
import StockPriceChart from '../components/StockPriceChart';
|
||||
import {
|
||||
PipelineOverview,
|
||||
AgentReportCard,
|
||||
PipelineFlowchart,
|
||||
DebateViewer,
|
||||
RiskDebateViewer,
|
||||
DataSourcesPanel
|
||||
} from '../components/pipeline';
|
||||
import { api } from '../services/api';
|
||||
import { useSettings } from '../contexts/SettingsContext';
|
||||
import type { FullPipelineData, AgentType } from '../types/pipeline';
|
||||
import type { FullPipelineData, PipelineStep, PipelineStepStatus } from '../types/pipeline';
|
||||
|
||||
// Type for real backtest data from API
|
||||
interface BacktestResult {
|
||||
date: string;
|
||||
decision: string;
|
||||
return1d: number | null;
|
||||
return1w: number | null;
|
||||
predictionCorrect: boolean | null;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
// Type for prediction stats calculated from real data
|
||||
interface PredictionStats {
|
||||
totalPredictions: number;
|
||||
correctPredictions: number;
|
||||
accuracy: number;
|
||||
avgReturn: number;
|
||||
buyAccuracy: number;
|
||||
sellAccuracy: number;
|
||||
holdAccuracy: number;
|
||||
}
|
||||
|
||||
type TabType = 'overview' | 'pipeline' | 'debates' | 'data';
|
||||
|
||||
|
|
@ -37,22 +58,229 @@ export default function StockDetail() {
|
|||
const [isAnalysisRunning, setIsAnalysisRunning] = useState(false);
|
||||
const [analysisStatus, setAnalysisStatus] = useState<string | null>(null);
|
||||
const [analysisProgress, setAnalysisProgress] = useState<string | null>(null);
|
||||
const [analysisSteps, setAnalysisSteps] = useState<{ completed: number; total: number } | null>(null);
|
||||
|
||||
const stock = NIFTY_50_STOCKS.find(s => s.symbol === symbol);
|
||||
const latestRecommendation = sampleRecommendations[0];
|
||||
const analysis = latestRecommendation?.analysis[symbol || ''];
|
||||
const history = symbol ? getStockHistory(symbol) : [];
|
||||
|
||||
// Get price history and prediction points for the chart
|
||||
const priceHistory = useMemo(() => {
|
||||
return symbol ? getExtendedPriceHistory(symbol, 60) : [];
|
||||
// API-first loading for recommendation data
|
||||
const [latestRecommendation, setLatestRecommendation] = useState<DailyRecommendation | null>(null);
|
||||
const [analysis, setAnalysis] = useState<StockAnalysis | undefined>(undefined);
|
||||
const [history, setHistory] = useState<Array<{ date: string; decision: string; confidence?: string; risk?: string }>>([]);
|
||||
// Fetch recommendation and stock history from API
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch latest recommendation from API
|
||||
const rec = await api.getLatestRecommendation();
|
||||
if (rec && rec.analysis && Object.keys(rec.analysis).length > 0) {
|
||||
setLatestRecommendation(rec);
|
||||
setAnalysis(rec.analysis[symbol || '']);
|
||||
} else {
|
||||
// Fallback to static data
|
||||
const mockRec = sampleRecommendations[0];
|
||||
setLatestRecommendation(mockRec);
|
||||
setAnalysis(mockRec?.analysis[symbol || '']);
|
||||
}
|
||||
} catch {
|
||||
// Fallback to static data
|
||||
const mockRec = sampleRecommendations[0];
|
||||
setLatestRecommendation(mockRec);
|
||||
setAnalysis(mockRec?.analysis[symbol || '']);
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch stock history from API
|
||||
const historyData = await api.getStockHistory(symbol || '');
|
||||
if (historyData && historyData.history && historyData.history.length > 0) {
|
||||
setHistory(historyData.history);
|
||||
} else {
|
||||
// Fallback to static data
|
||||
setHistory(symbol ? getStaticStockHistory(symbol) : []);
|
||||
}
|
||||
} catch {
|
||||
// Fallback to static data
|
||||
setHistory(symbol ? getStaticStockHistory(symbol) : []);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [symbol]);
|
||||
|
||||
// State for real backtest data from API
|
||||
const [backtestResults, setBacktestResults] = useState<BacktestResult[]>([]);
|
||||
const [isLoadingBacktest, setIsLoadingBacktest] = useState(false);
|
||||
|
||||
// Fetch real backtest data for all history entries
|
||||
const fetchBacktestData = useCallback(async () => {
|
||||
if (!symbol || history.length === 0) return;
|
||||
|
||||
setIsLoadingBacktest(true);
|
||||
|
||||
const results: BacktestResult[] = [];
|
||||
|
||||
for (const entry of history) {
|
||||
try {
|
||||
const backtest = await api.getBacktestResult(entry.date, symbol);
|
||||
|
||||
if (backtest.available) {
|
||||
// Calculate prediction correctness based on 1-day return
|
||||
// BUY is correct if return > 0, HOLD is correct if return > 0, SELL is correct if return < 0
|
||||
let predictionCorrect: boolean | null = null;
|
||||
if (backtest.actual_return_1d !== undefined && backtest.actual_return_1d !== null) {
|
||||
if (entry.decision === 'BUY' || entry.decision === 'HOLD') {
|
||||
// BUY and HOLD are correct if stock price went up
|
||||
predictionCorrect = backtest.actual_return_1d > 0;
|
||||
} else if (entry.decision === 'SELL') {
|
||||
// SELL is correct if stock price went down
|
||||
predictionCorrect = backtest.actual_return_1d < 0;
|
||||
}
|
||||
}
|
||||
|
||||
results.push({
|
||||
date: entry.date,
|
||||
decision: entry.decision,
|
||||
return1d: backtest.actual_return_1d ?? null,
|
||||
return1w: backtest.actual_return_1w ?? null,
|
||||
predictionCorrect,
|
||||
});
|
||||
} else {
|
||||
// No backtest data available for this date
|
||||
results.push({
|
||||
date: entry.date,
|
||||
decision: entry.decision,
|
||||
return1d: null,
|
||||
return1w: null,
|
||||
predictionCorrect: null,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch backtest for ${entry.date}:`, err);
|
||||
results.push({
|
||||
date: entry.date,
|
||||
decision: entry.decision,
|
||||
return1d: null,
|
||||
return1w: null,
|
||||
predictionCorrect: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setBacktestResults(results);
|
||||
setIsLoadingBacktest(false);
|
||||
}, [symbol, history]);
|
||||
|
||||
// Fetch backtest data when symbol changes
|
||||
useEffect(() => {
|
||||
fetchBacktestData();
|
||||
}, [fetchBacktestData]);
|
||||
|
||||
// Calculate prediction stats from real backtest data
|
||||
const predictionStats = useMemo((): PredictionStats | null => {
|
||||
if (backtestResults.length === 0) return null;
|
||||
|
||||
const resultsWithData = backtestResults.filter(r => r.return1d !== null);
|
||||
if (resultsWithData.length === 0) return null;
|
||||
|
||||
let correct = 0;
|
||||
let totalReturn = 0;
|
||||
let buyTotal = 0, buyCorrect = 0;
|
||||
let sellTotal = 0, sellCorrect = 0;
|
||||
let holdTotal = 0, holdCorrect = 0;
|
||||
|
||||
for (const result of resultsWithData) {
|
||||
if (result.return1d !== null) {
|
||||
totalReturn += result.return1d;
|
||||
}
|
||||
if (result.predictionCorrect !== null) {
|
||||
if (result.predictionCorrect) correct++;
|
||||
|
||||
if (result.decision === 'BUY') {
|
||||
buyTotal++;
|
||||
if (result.predictionCorrect) buyCorrect++;
|
||||
} else if (result.decision === 'SELL') {
|
||||
sellTotal++;
|
||||
if (result.predictionCorrect) sellCorrect++;
|
||||
} else {
|
||||
holdTotal++;
|
||||
if (result.predictionCorrect) holdCorrect++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalWithResult = resultsWithData.filter(r => r.predictionCorrect !== null).length;
|
||||
|
||||
return {
|
||||
totalPredictions: resultsWithData.length,
|
||||
correctPredictions: correct,
|
||||
accuracy: totalWithResult > 0 ? Math.round((correct / totalWithResult) * 100) : 0,
|
||||
avgReturn: resultsWithData.length > 0 ? Math.round((totalReturn / resultsWithData.length) * 10) / 10 : 0,
|
||||
buyAccuracy: buyTotal > 0 ? Math.round((buyCorrect / buyTotal) * 100) : 0,
|
||||
sellAccuracy: sellTotal > 0 ? Math.round((sellCorrect / sellTotal) * 100) : 0,
|
||||
holdAccuracy: holdTotal > 0 ? Math.round((holdCorrect / holdTotal) * 100) : 0,
|
||||
};
|
||||
}, [backtestResults]);
|
||||
|
||||
// Real price history from API
|
||||
const [realPriceHistory, setRealPriceHistory] = useState<Array<{ date: string; price: number }>>([]);
|
||||
const [isLoadingPrices, setIsLoadingPrices] = useState(false);
|
||||
|
||||
// Fetch real price history from yfinance via backend
|
||||
useEffect(() => {
|
||||
if (!symbol) return;
|
||||
|
||||
const fetchPrices = async () => {
|
||||
setIsLoadingPrices(true);
|
||||
try {
|
||||
const data = await api.getStockPriceHistory(symbol, 90);
|
||||
if (data.prices && data.prices.length > 0) {
|
||||
setRealPriceHistory(data.prices);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch price history:', err);
|
||||
} finally {
|
||||
setIsLoadingPrices(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPrices();
|
||||
}, [symbol]);
|
||||
|
||||
// Build prediction points from real history data (API-sourced dates + decisions)
|
||||
const predictionPoints = useMemo(() => {
|
||||
return symbol && priceHistory.length > 0
|
||||
? getPredictionPointsWithPrices(symbol, priceHistory)
|
||||
: [];
|
||||
}, [symbol, priceHistory]);
|
||||
if (history.length === 0 || realPriceHistory.length === 0) return [];
|
||||
|
||||
const priceDateMap = new Map(realPriceHistory.map(p => [p.date, p.price]));
|
||||
const MAX_DATE_TOLERANCE_MS = 4 * 24 * 60 * 60 * 1000; // 4 days max (handles weekends/holidays)
|
||||
|
||||
return history
|
||||
.map(entry => {
|
||||
// Find exact date match first
|
||||
const price = priceDateMap.get(entry.date);
|
||||
if (price !== undefined) {
|
||||
return { date: entry.date, decision: entry.decision as 'BUY' | 'SELL' | 'HOLD', price };
|
||||
}
|
||||
|
||||
// Find closest date within tolerance (skip if prediction date is outside price range)
|
||||
const entryTime = new Date(entry.date).getTime();
|
||||
let closestPoint: { date: string; price: number } | null = null;
|
||||
let closestDiff = Infinity;
|
||||
for (const p of realPriceHistory) {
|
||||
const diff = Math.abs(new Date(p.date).getTime() - entryTime);
|
||||
if (diff < closestDiff) {
|
||||
closestDiff = diff;
|
||||
closestPoint = p;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestPoint && closestDiff <= MAX_DATE_TOLERANCE_MS) {
|
||||
return { date: closestPoint.date, decision: entry.decision as 'BUY' | 'SELL' | 'HOLD', price: closestPoint.price };
|
||||
}
|
||||
|
||||
return null; // Prediction date too far from any price data — skip
|
||||
})
|
||||
.filter((p): p is NonNullable<typeof p> => p !== null);
|
||||
}, [history, realPriceHistory]);
|
||||
|
||||
// Function to fetch pipeline data
|
||||
const fetchPipelineData = async (forceRefresh = false) => {
|
||||
|
|
@ -117,6 +345,31 @@ export default function StockDetail() {
|
|||
setAnalysisStatus('starting');
|
||||
setAnalysisProgress('Starting analysis...');
|
||||
|
||||
// Auto-switch to pipeline tab so user sees live progress
|
||||
setActiveTab('pipeline');
|
||||
|
||||
// Step ordering for pipeline visualization
|
||||
const STEP_ORDER = [
|
||||
'market_analyst', 'social_media_analyst', 'news_analyst', 'fundamentals_analyst',
|
||||
'bull_researcher', 'bear_researcher', 'research_manager', 'trader',
|
||||
'aggressive_analyst', 'conservative_analyst', 'neutral_analyst', 'risk_manager',
|
||||
];
|
||||
|
||||
// Initialize pipeline data with all-pending steps
|
||||
setPipelineData({
|
||||
date: latestRecommendation.date,
|
||||
symbol: symbol,
|
||||
agent_reports: {},
|
||||
debates: {},
|
||||
pipeline_steps: STEP_ORDER.map((name, idx) => ({
|
||||
step_number: idx + 1,
|
||||
step_name: name,
|
||||
status: 'pending' as PipelineStepStatus,
|
||||
})),
|
||||
data_sources: [],
|
||||
status: 'in_progress',
|
||||
});
|
||||
|
||||
try {
|
||||
// Trigger analysis with settings from context
|
||||
await api.runAnalysis(symbol, latestRecommendation.date, {
|
||||
|
|
@ -128,28 +381,103 @@ export default function StockDetail() {
|
|||
});
|
||||
setAnalysisStatus('running');
|
||||
|
||||
// Track poll count for periodic full data refresh
|
||||
let pollCount = 0;
|
||||
|
||||
// Poll for status
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const status = await api.getAnalysisStatus(symbol);
|
||||
setAnalysisProgress(status.progress || 'Processing...');
|
||||
|
||||
// Update step counts for progress indicator
|
||||
if (status.steps_completed !== undefined && status.steps_total !== undefined) {
|
||||
setAnalysisSteps({ completed: status.steps_completed, total: status.steps_total });
|
||||
}
|
||||
|
||||
// Build live pipeline data from status response
|
||||
if (status.pipeline_steps) {
|
||||
const livePipelineSteps: PipelineStep[] = STEP_ORDER.map((stepName, idx) => {
|
||||
const stepData = status.pipeline_steps?.[stepName];
|
||||
return {
|
||||
step_number: idx + 1,
|
||||
step_name: stepName,
|
||||
status: (stepData?.status as PipelineStepStatus) || 'pending',
|
||||
duration_ms: stepData?.duration_ms,
|
||||
};
|
||||
});
|
||||
|
||||
// Update pipeline data with live step statuses
|
||||
setPipelineData(prev => ({
|
||||
date: latestRecommendation?.date || prev?.date || '',
|
||||
symbol: symbol || prev?.symbol || '',
|
||||
agent_reports: prev?.agent_reports || {},
|
||||
debates: prev?.debates || {},
|
||||
pipeline_steps: livePipelineSteps,
|
||||
data_sources: prev?.data_sources || [],
|
||||
status: 'in_progress',
|
||||
}));
|
||||
}
|
||||
|
||||
// Every 5th poll (~10s), fetch full pipeline data for agent reports/debates
|
||||
pollCount++;
|
||||
if (pollCount % 5 === 0) {
|
||||
try {
|
||||
const fullData = await api.getPipelineData(latestRecommendation.date, symbol, true);
|
||||
if (fullData && (fullData.agent_reports || fullData.debates)) {
|
||||
setPipelineData(prev => ({
|
||||
...prev!,
|
||||
agent_reports: fullData.agent_reports || prev?.agent_reports || {},
|
||||
debates: fullData.debates || prev?.debates || {},
|
||||
data_sources: fullData.data_sources || prev?.data_sources || [],
|
||||
// Keep live step statuses if available, otherwise use fetched
|
||||
pipeline_steps: prev?.pipeline_steps?.some(s => s.status === 'running')
|
||||
? prev!.pipeline_steps
|
||||
: fullData.pipeline_steps || prev?.pipeline_steps || [],
|
||||
}));
|
||||
}
|
||||
} catch { /* ignore full data refresh errors during analysis */ }
|
||||
}
|
||||
|
||||
if (status.status === 'completed') {
|
||||
clearInterval(pollInterval);
|
||||
setIsAnalysisRunning(false);
|
||||
setAnalysisStatus('completed');
|
||||
setAnalysisProgress(`✓ Analysis complete: ${status.decision || 'Done'}`);
|
||||
// Refresh data to show results
|
||||
setAnalysisProgress(`Analysis complete: ${status.decision || 'Done'}`);
|
||||
// Refresh recommendation and pipeline data to show final results
|
||||
try {
|
||||
const rec = await api.getLatestRecommendation();
|
||||
if (rec && rec.analysis && Object.keys(rec.analysis).length > 0) {
|
||||
setLatestRecommendation(rec);
|
||||
setAnalysis(rec.analysis[symbol || '']);
|
||||
}
|
||||
const historyData = await api.getStockHistory(symbol || '');
|
||||
if (historyData?.history?.length > 0) {
|
||||
setHistory(historyData.history);
|
||||
}
|
||||
} catch { /* ignore refresh errors */ }
|
||||
await fetchPipelineData(true);
|
||||
fetchBacktestData();
|
||||
setTimeout(() => {
|
||||
setAnalysisProgress(null);
|
||||
setAnalysisStatus(null);
|
||||
setAnalysisSteps(null);
|
||||
}, 5000);
|
||||
} else if (status.status === 'error') {
|
||||
clearInterval(pollInterval);
|
||||
setIsAnalysisRunning(false);
|
||||
setAnalysisStatus('error');
|
||||
setAnalysisProgress(`✗ Error: ${status.error}`);
|
||||
setAnalysisProgress(`Error: ${status.error}`);
|
||||
} else if (status.status === 'cancelled') {
|
||||
clearInterval(pollInterval);
|
||||
setIsAnalysisRunning(false);
|
||||
setAnalysisStatus('cancelled');
|
||||
setAnalysisProgress('Analysis cancelled');
|
||||
setTimeout(() => {
|
||||
setAnalysisProgress(null);
|
||||
setAnalysisStatus(null);
|
||||
setAnalysisSteps(null);
|
||||
}, 3000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to poll analysis status:', err);
|
||||
|
|
@ -173,6 +501,25 @@ export default function StockDetail() {
|
|||
}
|
||||
};
|
||||
|
||||
// Cancel Analysis handler
|
||||
const handleCancelAnalysis = async () => {
|
||||
if (!symbol) return;
|
||||
|
||||
try {
|
||||
await api.cancelAnalysis(symbol);
|
||||
setIsAnalysisRunning(false);
|
||||
setAnalysisStatus('cancelled');
|
||||
setAnalysisProgress('Analysis cancelled');
|
||||
setTimeout(() => {
|
||||
setAnalysisProgress(null);
|
||||
setAnalysisStatus(null);
|
||||
setAnalysisSteps(null);
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
console.error('Failed to cancel analysis:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!stock) {
|
||||
return (
|
||||
<div className="min-h-[60vh] flex items-center justify-center">
|
||||
|
|
@ -257,23 +604,87 @@ export default function StockDetail() {
|
|||
|
||||
{/* Analysis Details - Inline */}
|
||||
{analysis && (
|
||||
<div className="p-3 flex items-center gap-4 bg-gray-50/50 dark:bg-slate-700/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Decision:</span>
|
||||
<div className="p-2 sm:p-3 flex flex-wrap items-center gap-2 sm:gap-4 bg-gray-50/50 dark:bg-slate-700/50">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<span className="text-[10px] sm:text-xs text-gray-500 dark:text-gray-400">Decision:</span>
|
||||
<DecisionBadge decision={analysis.decision} size="small" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Confidence:</span>
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<span className="text-[10px] sm:text-xs text-gray-500 dark:text-gray-400">Confidence:</span>
|
||||
<ConfidenceBadge confidence={analysis.confidence} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Risk:</span>
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<span className="text-[10px] sm:text-xs text-gray-500 dark:text-gray-400">Risk:</span>
|
||||
<RiskBadge risk={analysis.risk} />
|
||||
</div>
|
||||
{analysis.hold_days && analysis.decision !== 'SELL' && (
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<span className="text-[10px] sm:text-xs text-gray-500 dark:text-gray-400">Hold:</span>
|
||||
<HoldDaysBadge holdDays={analysis.hold_days} decision={analysis.decision} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Action Buttons Row - Always visible */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Run Analysis Button */}
|
||||
<button
|
||||
onClick={handleRunAnalysis}
|
||||
disabled={isAnalysisRunning || isRefreshing || isLoadingPipeline}
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
|
||||
${isAnalysisRunning
|
||||
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||
: 'bg-nifty-600 text-white hover:bg-nifty-700 shadow-sm'
|
||||
}
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
`}
|
||||
title="Run AI analysis for this stock"
|
||||
>
|
||||
{isAnalysisRunning ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Play className="w-4 h-4" />
|
||||
)}
|
||||
{isAnalysisRunning ? 'Analyzing...' : 'Run Analysis'}
|
||||
</button>
|
||||
|
||||
{/* Cancel Analysis Button - only shown when analysis is running */}
|
||||
{isAnalysisRunning && (
|
||||
<button
|
||||
onClick={handleCancelAnalysis}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-900/50"
|
||||
title="Cancel running analysis"
|
||||
>
|
||||
<Square className="w-3.5 h-3.5 fill-current" />
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Refresh Button */}
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing || isLoadingPipeline || isAnalysisRunning}
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
|
||||
bg-gray-100 dark:bg-slate-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-slate-600
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
`}
|
||||
title="Refresh pipeline data"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
{isRefreshing ? 'Refreshing...' : 'Refresh'}
|
||||
</button>
|
||||
|
||||
{lastRefresh && (
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 ml-auto">
|
||||
Updated: {lastRefresh}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="card p-1 flex gap-1 overflow-x-auto">
|
||||
{TABS.map(tab => {
|
||||
|
|
@ -284,7 +695,7 @@ export default function StockDetail() {
|
|||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`
|
||||
flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all whitespace-nowrap
|
||||
flex items-center gap-2 px-3 sm:px-4 py-2 rounded-lg text-sm font-medium transition-all whitespace-nowrap
|
||||
${isActive
|
||||
? 'bg-nifty-600 text-white shadow-md'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700'
|
||||
|
|
@ -292,71 +703,51 @@ export default function StockDetail() {
|
|||
`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
<span className="hidden sm:inline">{tab.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Action Buttons - Show on non-overview tabs */}
|
||||
{activeTab !== 'overview' && (
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{lastRefresh && (
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||
Updated: {lastRefresh}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Run Analysis Button */}
|
||||
<button
|
||||
onClick={handleRunAnalysis}
|
||||
disabled={isAnalysisRunning || isRefreshing || isLoadingPipeline}
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
|
||||
${isAnalysisRunning
|
||||
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||
: 'bg-nifty-600 text-white hover:bg-nifty-700'
|
||||
}
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
`}
|
||||
title="Run AI analysis for this stock"
|
||||
>
|
||||
{isAnalysisRunning ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Play className="w-4 h-4" />
|
||||
)}
|
||||
{isAnalysisRunning ? 'Analyzing...' : 'Run Analysis'}
|
||||
</button>
|
||||
|
||||
{/* Refresh Button */}
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing || isLoadingPipeline || isAnalysisRunning}
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
|
||||
text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
`}
|
||||
title="Refresh pipeline data"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
{isRefreshing ? 'Refreshing...' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Analysis Progress Banner */}
|
||||
{analysisProgress && (
|
||||
<div className={`p-3 rounded-lg text-sm font-medium flex items-center gap-2 ${
|
||||
<div className={`rounded-lg overflow-hidden ${
|
||||
analysisStatus === 'completed'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
|
||||
? 'bg-green-100 dark:bg-green-900/30'
|
||||
: 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'
|
||||
? 'bg-red-100 dark:bg-red-900/30'
|
||||
: analysisStatus === 'cancelled'
|
||||
? 'bg-amber-100 dark:bg-amber-900/30'
|
||||
: 'bg-blue-100 dark:bg-blue-900/30'
|
||||
}`}>
|
||||
{isAnalysisRunning && <Loader2 className="w-4 h-4 animate-spin" />}
|
||||
{analysisProgress}
|
||||
<div className={`p-3 text-sm font-medium flex items-center gap-2 ${
|
||||
analysisStatus === 'completed'
|
||||
? 'text-green-800 dark:text-green-300'
|
||||
: analysisStatus === 'error'
|
||||
? 'text-red-800 dark:text-red-300'
|
||||
: analysisStatus === 'cancelled'
|
||||
? 'text-amber-800 dark:text-amber-300'
|
||||
: 'text-blue-800 dark:text-blue-300'
|
||||
}`}>
|
||||
{isAnalysisRunning && <Loader2 className="w-4 h-4 animate-spin flex-shrink-0" />}
|
||||
<span className="flex-1">{analysisProgress}</span>
|
||||
{analysisSteps && (
|
||||
<span className="text-xs opacity-75 flex-shrink-0">
|
||||
{analysisSteps.completed}/{analysisSteps.total}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Step progress bar */}
|
||||
{analysisSteps && isAnalysisRunning && (
|
||||
<div className="px-3 pb-2">
|
||||
<div className="h-1.5 bg-blue-200/50 dark:bg-blue-800/30 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 dark:bg-blue-400 rounded-full transition-all duration-700 ease-out"
|
||||
style={{ width: `${Math.round((analysisSteps.completed / analysisSteps.total) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -377,83 +768,240 @@ export default function StockDetail() {
|
|||
{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>
|
||||
<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>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 ml-auto">
|
||||
{realPriceHistory.length > 0 ? `${realPriceHistory.length} trading days` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-4 bg-white dark:bg-slate-800">
|
||||
</div>
|
||||
<div className="p-4 bg-white dark:bg-slate-800">
|
||||
{isLoadingPrices ? (
|
||||
<div className="h-64 flex items-center justify-center text-gray-400 dark:text-gray-500">
|
||||
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
||||
Loading price data...
|
||||
</div>
|
||||
) : realPriceHistory.length > 0 ? (
|
||||
<StockPriceChart
|
||||
priceHistory={priceHistory}
|
||||
priceHistory={realPriceHistory}
|
||||
predictions={predictionPoints}
|
||||
symbol={symbol || ''}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
) : (
|
||||
<div className="h-64 flex items-center justify-center text-gray-400 dark:text-gray-500">
|
||||
No price data available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* AI Analysis Panel */}
|
||||
{analysis && getRawAnalysis(symbol || '') && (
|
||||
{analysis && (analysis.raw_analysis || getRawAnalysis(symbol || '')) && (
|
||||
<AIAnalysisPanel
|
||||
analysis={getRawAnalysis(symbol || '') || ''}
|
||||
analysis={analysis.raw_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">
|
||||
{/* Prediction Accuracy Stats */}
|
||||
{predictionStats && (
|
||||
<section className="card overflow-hidden">
|
||||
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gradient-to-r from-nifty-50 to-blue-50 dark:from-slate-800 dark:to-slate-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target 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">Prediction Accuracy</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{/* Main accuracy meter */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="relative w-20 h-20 flex-shrink-0">
|
||||
<svg className="w-20 h-20 transform -rotate-90">
|
||||
<circle
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="36"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
fill="transparent"
|
||||
className="text-gray-200 dark:text-slate-700"
|
||||
/>
|
||||
<circle
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="36"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
fill="transparent"
|
||||
strokeDasharray={`${(predictionStats.accuracy / 100) * 226} 226`}
|
||||
className={predictionStats.accuracy >= 70 ? 'text-green-500' : predictionStats.accuracy >= 50 ? 'text-amber-500' : 'text-red-500'}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className={`text-lg font-bold ${predictionStats.accuracy >= 70 ? 'text-green-600 dark:text-green-400' : predictionStats.accuracy >= 50 ? 'text-amber-600 dark:text-amber-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{predictionStats.accuracy}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{predictionStats.correctPredictions} of {predictionStats.totalPredictions} predictions correct
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Avg. 1-day return: <span className={predictionStats.avgReturn >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}>
|
||||
{predictionStats.avgReturn >= 0 ? '+' : ''}{predictionStats.avgReturn}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Accuracy by decision type */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-2 text-center">
|
||||
<div className="text-xs text-green-600 dark:text-green-400 font-medium mb-0.5">BUY</div>
|
||||
<div className="text-sm font-bold text-green-700 dark:text-green-300">{predictionStats.buyAccuracy}%</div>
|
||||
</div>
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-2 text-center">
|
||||
<div className="text-xs text-amber-600 dark:text-amber-400 font-medium mb-0.5">HOLD</div>
|
||||
<div className="text-sm font-bold text-amber-700 dark:text-amber-300">{predictionStats.holdAccuracy}%</div>
|
||||
</div>
|
||||
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-2 text-center">
|
||||
<div className="text-xs text-red-600 dark:text-red-400 font-medium mb-0.5">SELL</div>
|
||||
<div className="text-sm font-bold text-red-700 dark:text-red-300">{predictionStats.sellAccuracy}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Quick Stats Grid */}
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<div className="card p-2.5 text-center bg-gradient-to-br from-gray-50 to-gray-100 dark:from-slate-800 dark:to-slate-700">
|
||||
<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 className="text-[10px] text-gray-500 dark:text-gray-400">Total</div>
|
||||
</div>
|
||||
<div className="card p-2.5 text-center">
|
||||
<div className="card p-2.5 text-center bg-gradient-to-br from-green-50 to-green-100/50 dark:from-green-900/20 dark:to-green-900/10">
|
||||
<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 className="text-[10px] text-green-600 dark:text-green-400">Buy</div>
|
||||
</div>
|
||||
<div className="card p-2.5 text-center">
|
||||
<div className="card p-2.5 text-center bg-gradient-to-br from-amber-50 to-amber-100/50 dark:from-amber-900/20 dark:to-amber-900/10">
|
||||
<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 className="text-[10px] text-amber-600 dark:text-amber-400">Hold</div>
|
||||
</div>
|
||||
<div className="card p-2.5 text-center">
|
||||
<div className="card p-2.5 text-center bg-gradient-to-br from-red-50 to-red-100/50 dark:from-red-900/20 dark:to-red-900/10">
|
||||
<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 className="text-[10px] text-red-600 dark:text-red-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>
|
||||
{/* Recommendation History with Real Outcomes */}
|
||||
<section className="card overflow-hidden">
|
||||
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gradient-to-r from-gray-50 to-slate-50 dark:from-slate-800 dark:to-slate-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<BarChart3 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">Recommendation History</h2>
|
||||
{isLoadingBacktest && (
|
||||
<Loader2 className="w-3 h-3 animate-spin text-nifty-500" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-slate-600 px-2 py-0.5 rounded-full">
|
||||
Real 1-Day Returns
|
||||
</span>
|
||||
</div>
|
||||
</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',
|
||||
})}
|
||||
{isLoadingBacktest ? (
|
||||
<div className="p-8 text-center">
|
||||
<Loader2 className="w-8 h-8 text-nifty-500 animate-spin mx-auto mb-3" />
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Fetching real market data...</p>
|
||||
</div>
|
||||
) : backtestResults.length > 0 ? (
|
||||
<div className="divide-y divide-gray-100 dark:divide-slate-700 max-h-[320px] overflow-y-auto">
|
||||
{backtestResults.map((entry, idx) => (
|
||||
<div key={idx} className="px-3 py-2.5 flex items-center gap-3 hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors">
|
||||
{/* Date */}
|
||||
<div className="w-16 flex-shrink-0">
|
||||
<div className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
{new Date(entry.date).toLocaleDateString('en-IN', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
})}
|
||||
</div>
|
||||
<div className="text-[10px] text-gray-400 dark:text-gray-500">
|
||||
{new Date(entry.date).toLocaleDateString('en-IN', { weekday: 'short' })}
|
||||
</div>
|
||||
</div>
|
||||
<DecisionBadge decision={entry.decision} size="small" />
|
||||
|
||||
{/* Decision Badge + Hold Days */}
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||
<DecisionBadge decision={entry.decision as 'BUY' | 'SELL' | 'HOLD'} size="small" />
|
||||
{entry.holdDays && entry.decision !== 'SELL' && (
|
||||
<span className="text-[10px] text-blue-600 dark:text-blue-400 font-medium">{entry.holdDays}d</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Outcome - 1 Day Return */}
|
||||
{entry.return1d !== null ? (
|
||||
<>
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<div className={`text-sm font-semibold ${
|
||||
entry.return1d >= 0
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
}`}>
|
||||
{entry.return1d >= 0 ? '+' : ''}{entry.return1d.toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-[10px] text-gray-400 dark:text-gray-500">next day</div>
|
||||
</div>
|
||||
|
||||
{/* Prediction Result Icon */}
|
||||
<div className="flex-shrink-0">
|
||||
{entry.predictionCorrect !== null ? (
|
||||
entry.predictionCorrect ? (
|
||||
<div className="flex items-center gap-1 px-2 py-0.5 bg-green-100 dark:bg-green-900/30 rounded-full">
|
||||
<CheckCircle className="w-3 h-3 text-green-600 dark:text-green-400" />
|
||||
<span className="text-[10px] font-medium text-green-700 dark:text-green-400">Correct</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 px-2 py-0.5 bg-red-100 dark:bg-red-900/30 rounded-full">
|
||||
<XCircle className="w-3 h-3 text-red-600 dark:text-red-400" />
|
||||
<span className="text-[10px] font-medium text-red-700 dark:text-red-400">Wrong</span>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<span className="text-[10px] text-gray-400">N/A</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 text-xs text-gray-400 dark:text-gray-500 italic">
|
||||
Awaiting market data...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : history.length > 0 ? (
|
||||
<div className="p-8 text-center">
|
||||
<AlertTriangle className="w-10 h-10 text-amber-400 mx-auto mb-3" />
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Unable to fetch real market data</p>
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">Check if backend service is running</p>
|
||||
</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 className="p-8 text-center">
|
||||
<Calendar className="w-10 h-10 text-gray-300 dark:text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">No recommendation history yet</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
|
@ -461,37 +1009,11 @@ export default function StockDetail() {
|
|||
)}
|
||||
|
||||
{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>
|
||||
<PipelineFlowchart
|
||||
pipelineData={pipelineData}
|
||||
isAnalyzing={isAnalysisRunning}
|
||||
isLoading={isLoadingPipeline}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'debates' && (
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Loading…
Reference in New Issue