Skip to content

Commit 5d7092d

Browse files
committed
Add date filter to TradeHistoryTable and improve analysis progress tracking
Introduces a date filter with navigation controls to the TradeHistoryTable, allowing users to view trades by specific day. Enhances UnifiedAnalysisHistory by improving agent completion percentage calculation, supporting both workflow_steps and agent_insights data. Adds initial loading state to analysis controls and updates workflow state management for better UX during analysis startup.
1 parent 5fa0bb6 commit 5d7092d

File tree

5 files changed

+222
-73
lines changed

5 files changed

+222
-73
lines changed

src/components/TradeHistoryTable.tsx

Lines changed: 113 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
33
import { Badge } from "@/components/ui/badge";
44
import { Button } from "@/components/ui/button";
55
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
6-
import { ArrowUpRight, ArrowDownRight, Clock, CheckCircle, XCircle, TrendingUp, RefreshCw, Loader2, ExternalLink, FileText, BarChart3, Calendar, Package } from "lucide-react";
6+
import { ArrowUpRight, ArrowDownRight, Clock, CheckCircle, XCircle, TrendingUp, RefreshCw, Loader2, ExternalLink, FileText, BarChart3, Calendar, Package, ChevronLeft, ChevronRight, CalendarIcon } from "lucide-react";
77
import { useAuth } from "@/lib/auth";
88
import { supabase } from "@/lib/supabase";
99
import { getCachedSession } from "@/lib/cachedAuth";
@@ -51,17 +51,73 @@ export default function TradeHistoryTable() {
5151
const { apiSettings, user } = useAuth();
5252
const { toast } = useToast();
5353

54+
// Date filter states - default to today (using local date to avoid timezone issues)
55+
const today = new Date();
56+
const todayString = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
57+
const [selectedDate, setSelectedDate] = useState<string>(todayString);
58+
59+
// Helper to format date display
60+
const getDateDisplay = () => {
61+
const today = new Date();
62+
const todayString = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
63+
64+
const yesterday = new Date();
65+
yesterday.setDate(yesterday.getDate() - 1);
66+
const yesterdayString = `${yesterday.getFullYear()}-${String(yesterday.getMonth() + 1).padStart(2, '0')}-${String(yesterday.getDate()).padStart(2, '0')}`;
67+
68+
if (selectedDate === todayString) return "Today";
69+
if (selectedDate === yesterdayString) return "Yesterday";
70+
71+
// Parse the date parts to avoid timezone issues
72+
const [year, month, day] = selectedDate.split('-').map(Number);
73+
const date = new Date(year, month - 1, day);
74+
75+
return date.toLocaleDateString('en-US', {
76+
month: 'short',
77+
day: 'numeric',
78+
year: 'numeric'
79+
});
80+
};
81+
82+
// Navigate date helper functions
83+
const navigateDate = (direction: 'prev' | 'next') => {
84+
const [year, month, day] = selectedDate.split('-').map(Number);
85+
const currentDate = new Date(year, month - 1, day);
86+
87+
if (direction === 'prev') {
88+
currentDate.setDate(currentDate.getDate() - 1);
89+
} else {
90+
currentDate.setDate(currentDate.getDate() + 1);
91+
}
92+
93+
const newDateString = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(currentDate.getDate()).padStart(2, '0')}`;
94+
setSelectedDate(newDateString);
95+
};
96+
97+
const jumpToToday = () => {
98+
const today = new Date();
99+
const todayString = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
100+
setSelectedDate(todayString);
101+
};
102+
54103
// Fetch all trades from trading_actions table
55104
const fetchAllTrades = async () => {
56105
if (!user?.id) return;
57106

58107
setLoading(true);
59108
try {
60-
// Get all trading actions for this user
109+
// Build date range for the selected date using local date parsing
110+
const [year, month, day] = selectedDate.split('-').map(Number);
111+
const startOfDay = new Date(year, month - 1, day, 0, 0, 0, 0);
112+
const endOfDay = new Date(year, month - 1, day, 23, 59, 59, 999);
113+
114+
// Get trading actions for this user within the selected date
61115
const { data, error } = await supabase
62116
.from('trading_actions')
63117
.select('*')
64118
.eq('user_id', user.id)
119+
.gte('created_at', startOfDay.toISOString())
120+
.lte('created_at', endOfDay.toISOString())
65121
.order('created_at', { ascending: false });
66122

67123
if (error) throw error;
@@ -262,7 +318,7 @@ export default function TradeHistoryTable() {
262318
} else {
263319
console.log('No Alpaca credentials found, skipping order status update');
264320
}
265-
}, [apiSettings, user]);
321+
}, [apiSettings, user, selectedDate]); // Added selectedDate dependency
266322

267323
// Periodically update Alpaca order status
268324
useEffect(() => {
@@ -757,27 +813,61 @@ export default function TradeHistoryTable() {
757813
<div className="flex items-center justify-between">
758814
<CardTitle className="text-foreground flex items-center gap-2">
759815
<TrendingUp className="h-5 w-5 text-primary" />
760-
Complete Trade History
816+
Trade History
761817
</CardTitle>
762-
<Button
763-
variant="ghost"
764-
size="icon"
765-
className="h-7 w-7"
766-
onClick={() => {
767-
fetchAllTrades();
768-
const hasCredentials = apiSettings?.alpaca_paper_api_key || apiSettings?.alpaca_live_api_key;
769-
if (hasCredentials) {
770-
updateAlpacaOrderStatus();
771-
}
772-
}}
773-
disabled={loading}
774-
>
775-
{loading ? (
776-
<Loader2 className="h-4 w-4 animate-spin" />
777-
) : (
778-
<RefreshCw className="h-4 w-4" />
779-
)}
780-
</Button>
818+
819+
<div className="flex items-center gap-1">
820+
<Button
821+
variant="ghost"
822+
size="sm"
823+
onClick={() => navigateDate('prev')}
824+
className="h-8 w-8 p-0 hover:bg-[#fc0]/10 hover:text-[#fc0]"
825+
>
826+
<ChevronLeft className="h-4 w-4" />
827+
</Button>
828+
829+
<Button
830+
variant="outline"
831+
size="sm"
832+
className="px-3 min-w-[140px] hover:border-[#fc0] hover:bg-[#fc0]/10 hover:text-[#fc0] transition-all duration-200"
833+
onClick={jumpToToday}
834+
>
835+
<CalendarIcon className="h-4 w-4 mr-2" />
836+
{getDateDisplay()}
837+
</Button>
838+
839+
<Button
840+
variant="ghost"
841+
size="sm"
842+
onClick={() => navigateDate('next')}
843+
disabled={selectedDate === todayString}
844+
className="h-8 w-8 p-0 hover:bg-[#fc0]/10 hover:text-[#fc0] disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:text-muted-foreground"
845+
>
846+
<ChevronRight className="h-4 w-4" />
847+
</Button>
848+
849+
<div className="ml-2">
850+
<Button
851+
variant="ghost"
852+
size="icon"
853+
className="h-8 w-8"
854+
onClick={() => {
855+
fetchAllTrades();
856+
const hasCredentials = apiSettings?.alpaca_paper_api_key || apiSettings?.alpaca_live_api_key;
857+
if (hasCredentials) {
858+
updateAlpacaOrderStatus();
859+
}
860+
}}
861+
disabled={loading}
862+
>
863+
{loading ? (
864+
<Loader2 className="h-4 w-4 animate-spin" />
865+
) : (
866+
<RefreshCw className="h-4 w-4" />
867+
)}
868+
</Button>
869+
</div>
870+
</div>
781871
</div>
782872
</CardHeader>
783873

src/components/UnifiedAnalysisHistory.tsx

Lines changed: 63 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ interface RunningAnalysisItem {
6868
ticker: string;
6969
created_at: string;
7070
full_analysis?: any; // Add to store progress data
71+
agent_insights?: any; // Add to access agent completion data
7172
rebalance_request_id?: string; // To check if part of rebalance
7273
status?: AnalysisStatus; // To distinguish between pending and running
7374
}
@@ -80,42 +81,66 @@ interface RebalanceAnalysisGroup {
8081
}
8182

8283
// Calculate agent completion percentage for a single analysis
83-
const calculateAgentCompletion = (fullAnalysis: any, isRebalanceAnalysis: boolean = false): number => {
84-
if (!fullAnalysis?.messages) return 0;
85-
86-
// Define expected agents (including macro-analyst)
87-
// For rebalance analyses, exclude portfolio-manager as it runs at rebalance level
88-
const expectedAgents = [
89-
'macro-analyst', 'market-analyst', 'news-analyst', 'social-media-analyst', 'fundamentals-analyst',
90-
'bull-researcher', 'bear-researcher', 'research-manager',
91-
'risky-analyst', 'safe-analyst', 'neutral-analyst', 'risk-manager',
92-
'trader'
93-
];
94-
95-
// Only add portfolio-manager for standalone analyses (not part of rebalance)
96-
if (!isRebalanceAnalysis) {
97-
expectedAgents.push('portfolio-manager');
98-
}
99-
100-
const messages = fullAnalysis.messages || [];
101-
const completedAgents = new Set<string>();
102-
103-
messages.forEach((msg: any) => {
104-
if (msg.agent && msg.timestamp) {
105-
const normalizedAgent = msg.agent.toLowerCase().replace(/\s+/g, '-');
106-
completedAgents.add(normalizedAgent);
84+
// Similar to how GitHub Actions tracks workflow steps
85+
const calculateAgentCompletion = (analysisItem: any, isRebalanceAnalysis: boolean = false): number => {
86+
if (!analysisItem) return 0;
87+
88+
let totalAgents = 0;
89+
let completedAgents = 0;
90+
91+
// Primary method: Check workflow_steps structure (similar to GitHub Actions)
92+
// This is the most reliable way as it mirrors the workflow visualization
93+
if (analysisItem.full_analysis?.workflow_steps) {
94+
const workflowSteps = analysisItem.full_analysis.workflow_steps;
95+
96+
// Iterate through all phases and count agents
97+
Object.keys(workflowSteps).forEach(phase => {
98+
// Skip portfolio phase for rebalance analyses
99+
if (phase === 'portfolio' && isRebalanceAnalysis) {
100+
return;
101+
}
102+
103+
const phaseData = workflowSteps[phase];
104+
if (phaseData?.agents && Array.isArray(phaseData.agents)) {
105+
phaseData.agents.forEach((agent: any) => {
106+
totalAgents++;
107+
// Check if agent is completed (similar to GitHub Actions step status)
108+
if (agent.status === 'completed' || agent.status === 'complete') {
109+
completedAgents++;
110+
}
111+
});
112+
}
113+
});
114+
115+
// Return percentage if we found agents
116+
if (totalAgents > 0) {
117+
return Math.round((completedAgents / totalAgents) * 100);
107118
}
108-
});
119+
}
109120

110-
// Count matches
111-
let matchedAgents = 0;
112-
expectedAgents.forEach(agentKey => {
113-
if (completedAgents.has(agentKey)) {
114-
matchedAgents++;
121+
// Fallback method: Count agent_insights (for when workflow_steps is not available)
122+
// This counts completed agents by checking which ones have insights
123+
if (analysisItem.agent_insights && typeof analysisItem.agent_insights === 'object') {
124+
const insightKeys = Object.keys(analysisItem.agent_insights);
125+
126+
// Count valid agent insights (non-empty values)
127+
completedAgents = insightKeys.filter(key => {
128+
const value = analysisItem.agent_insights[key];
129+
return value !== null && value !== undefined && value !== '';
130+
}).length;
131+
132+
// Use a reasonable estimate for total agents
133+
// Based on the workflow: 5 analysis + 3 research + 1 trader + 4 risk + 1 portfolio
134+
totalAgents = isRebalanceAnalysis ? 13 : 14;
135+
136+
if (completedAgents > 0) {
137+
// Don't exceed 100%
138+
return Math.min(Math.round((completedAgents / totalAgents) * 100), 100);
115139
}
116-
});
140+
}
117141

118-
return expectedAgents.length > 0 ? (matchedAgents / expectedAgents.length) * 100 : 0;
142+
// If no data available, return 0
143+
return 0;
119144
};
120145

121146
export default function UnifiedAnalysisHistory() {
@@ -232,6 +257,7 @@ export default function UnifiedAnalysisHistory() {
232257
ticker: item.ticker,
233258
created_at: item.created_at,
234259
full_analysis: item.full_analysis,
260+
agent_insights: item.agent_insights, // Add this!
235261
rebalance_request_id: item.rebalance_request_id,
236262
status: status
237263
});
@@ -851,18 +877,18 @@ export default function UnifiedAnalysisHistory() {
851877

852878
<div className="flex items-center justify-between">
853879
<div className="flex-1">
854-
{item.status === ANALYSIS_STATUS.RUNNING && item.full_analysis && (
880+
{item.status === ANALYSIS_STATUS.RUNNING && (item.full_analysis || item.agent_insights) && (
855881
<div className="flex items-center gap-2 mr-4">
856882
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
857883
<div
858884
className="h-full bg-yellow-500 animate-pulse transition-all"
859885
style={{
860-
width: `${calculateAgentCompletion(item.full_analysis, !!item.rebalance_request_id)}%`
886+
width: `${calculateAgentCompletion(item, !!item.rebalance_request_id)}%`
861887
}}
862888
/>
863889
</div>
864890
<span className="text-xs text-yellow-600 dark:text-yellow-400">
865-
{Math.round(calculateAgentCompletion(item.full_analysis, !!item.rebalance_request_id))}%
891+
{Math.round(calculateAgentCompletion(item, !!item.rebalance_request_id))}%
866892
</span>
867893
</div>
868894
)}
@@ -1127,12 +1153,12 @@ export default function UnifiedAnalysisHistory() {
11271153
<div
11281154
className="h-full bg-yellow-500 animate-pulse transition-all"
11291155
style={{
1130-
width: `${calculateAgentCompletion(item.full_analysis, !!item.rebalance_request_id)}%`
1156+
width: `${calculateAgentCompletion(item, !!item.rebalance_request_id)}%`
11311157
}}
11321158
/>
11331159
</div>
11341160
<span className="text-xs text-yellow-600 dark:text-yellow-400">
1135-
{Math.round(calculateAgentCompletion(item.full_analysis, !!item.rebalance_request_id))}%
1161+
{Math.round(calculateAgentCompletion(item, !!item.rebalance_request_id))}%
11361162
</span>
11371163
</div>
11381164
)}

src/components/workflow/components/AnalysisControls.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Analysis controls component for starting and viewing analyses
33
*/
44

5-
import { Activity, Info, Play } from 'lucide-react';
5+
import { Activity, Info, Loader2, Play } from 'lucide-react';
66
import { Button } from '@/components/ui/button';
77
import StockTickerAutocomplete from '@/components/StockTickerAutocomplete';
88

@@ -13,6 +13,7 @@ interface AnalysisControlsProps {
1313
setSearchTicker: (value: string) => void;
1414
handleStartAnalysis: () => void;
1515
setShowAnalysisDetail: (value: boolean) => void;
16+
isInitialLoading?: boolean;
1617
}
1718

1819
export function AnalysisControls({
@@ -21,11 +22,20 @@ export function AnalysisControls({
2122
searchTicker,
2223
setSearchTicker,
2324
handleStartAnalysis,
24-
setShowAnalysisDetail
25+
setShowAnalysisDetail,
26+
isInitialLoading = false
2527
}: AnalysisControlsProps) {
2628
return (
2729
<div className="flex items-center justify-center p-2 rounded-lg mb-2 min-h-[36px]">
28-
{activeAnalysisTicker && isAnalyzing ? (
30+
{isInitialLoading ? (
31+
// Show loading state while checking for running analyses
32+
<div className="flex items-center justify-center w-full">
33+
<Loader2 className="w-4 h-4 mr-2 animate-spin text-muted-foreground" />
34+
<span className="text-sm text-muted-foreground">
35+
Checking for running analyses...
36+
</span>
37+
</div>
38+
) : activeAnalysisTicker && isAnalyzing ? (
2939
<div className="flex items-center justify-between w-full">
3040
<div className="flex items-center">
3141
<Activity className="w-4 h-4 mr-2 animate-pulse text-primary" />

src/components/workflow/hooks/useAnalysisState.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function useAnalysisState(updateWorkflowFromAnalysis: (analysis: any) =>
1414
const [isAnalyzing, setIsAnalyzing] = useState(false);
1515
const [runningAnalysesCount, setRunningAnalysesCount] = useState(0);
1616
const [isRebalanceContext, setIsRebalanceContext] = useState(false);
17+
const [isInitialLoading, setIsInitialLoading] = useState(true);
1718
const previousRunningRef = useRef<Set<string>>(new Set());
1819

1920
useEffect(() => {
@@ -28,6 +29,11 @@ export function useAnalysisState(updateWorkflowFromAnalysis: (analysis: any) =>
2829
setIsAnalyzing,
2930
setRunningAnalysesCount
3031
});
32+
33+
// Mark initial loading as complete after first check
34+
if (isInitialLoading) {
35+
setIsInitialLoading(false);
36+
}
3137
};
3238

3339
handleCheckRunningAnalyses();
@@ -87,6 +93,7 @@ export function useAnalysisState(updateWorkflowFromAnalysis: (analysis: any) =>
8793
setIsAnalyzing,
8894
runningAnalysesCount,
8995
isRebalanceContext,
90-
setIsRebalanceContext
96+
setIsRebalanceContext,
97+
isInitialLoading
9198
};
9299
}

0 commit comments

Comments
 (0)