Skip to content

Commit c0640b8

Browse files
committed
update: UI/UX
1 parent 7439998 commit c0640b8

File tree

8 files changed

+516
-227
lines changed

8 files changed

+516
-227
lines changed

src/components/AnalysisDetailModal.tsx

Lines changed: 76 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import React, { useState, useEffect } from "react";
22
import {
33
Dialog,
4-
DialogContent,
54
DialogDescription,
65
DialogHeader,
76
DialogTitle,
87
} from "@/components/ui/dialog";
8+
import * as DialogPrimitive from "@radix-ui/react-dialog";
9+
import { cn } from "@/lib/utils";
910
import { Badge } from "@/components/ui/badge";
1011
import { ScrollArea } from "@/components/ui/scroll-area";
1112
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -77,6 +78,26 @@ import { useAnalysisData } from "./analysis-detail/hooks/useAnalysisData";
7778
import { useOrderActions } from "./analysis-detail/hooks/useOrderActions";
7879
import { getDecisionIcon, getDecisionVariant } from "./analysis-detail/utils/statusHelpers";
7980

81+
// Custom DialogContent without the default close button
82+
const DialogContentNoClose = React.forwardRef<
83+
React.ElementRef<typeof DialogPrimitive.Content>,
84+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
85+
>(({ className, children, ...props }, ref) => (
86+
<DialogPrimitive.Portal>
87+
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
88+
<DialogPrimitive.Content
89+
ref={ref}
90+
className={cn(
91+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
92+
className
93+
)}
94+
{...props}
95+
>
96+
{children}
97+
</DialogPrimitive.Content>
98+
</DialogPrimitive.Portal>
99+
));
100+
DialogContentNoClose.displayName = "DialogContentNoClose";
80101

81102
export default function AnalysisDetailModal({ ticker, analysisId, isOpen, onClose, analysisDate, initialTab }: AnalysisDetailModalProps) {
82103
// Use extracted custom hooks
@@ -337,7 +358,7 @@ export default function AnalysisDetailModal({ ticker, analysisId, isOpen, onClos
337358

338359
return (
339360
<Dialog open={isOpen} onOpenChange={() => onClose()}>
340-
<DialogContent className="max-w-7xl max-h-[90vh] p-0">
361+
<DialogContentNoClose className="max-w-7xl max-h-[90vh] p-0">
341362
<DialogHeader className="px-6 pt-6 pb-4 border-b">
342363
<div className="flex items-center justify-between">
343364
<div className="flex items-center gap-3">
@@ -377,56 +398,69 @@ export default function AnalysisDetailModal({ ticker, analysisId, isOpen, onClos
377398
)}
378399
</div>
379400

380-
{/* Retry/Reactivate Button */}
381-
{analysisData && (
382-
<>
383-
{analysisData.status === ANALYSIS_STATUS.ERROR && (
384-
<Button
385-
variant="outline"
386-
size="sm"
387-
onClick={handleRetry}
388-
disabled={isRetrying}
389-
className="flex items-center gap-2"
390-
>
391-
{isRetrying ? (
392-
<Loader2 className="w-4 h-4 animate-spin" />
393-
) : (
394-
<RefreshCw className="w-4 h-4" />
395-
)}
396-
Retry Analysis
397-
</Button>
398-
)}
399-
400-
{(() => {
401-
const shouldShowReactivate = analysisData.status === ANALYSIS_STATUS.RUNNING && isAnalysisStale();
402-
console.log('Reactivate button check:', {
403-
status: analysisData.status,
404-
isRunning: analysisData.status === ANALYSIS_STATUS.RUNNING,
405-
isStale: isAnalysisStale(),
406-
shouldShow: shouldShowReactivate,
407-
ANALYSIS_STATUS
408-
});
409-
410-
return shouldShowReactivate && (
401+
{/* Action buttons container */}
402+
<div className="flex items-center gap-2">
403+
{/* Retry/Reactivate Button */}
404+
{analysisData && (
405+
<>
406+
{analysisData.status === ANALYSIS_STATUS.ERROR && (
411407
<Button
412408
variant="outline"
413409
size="sm"
414-
onClick={handleReactivate}
410+
onClick={handleRetry}
415411
disabled={isRetrying}
416412
className="flex items-center gap-2"
417-
title={`Last updated ${formatDistanceToNow(new Date(analysisData.updated_at))} ago`}
418413
>
419414
{isRetrying ? (
420415
<Loader2 className="w-4 h-4 animate-spin" />
421416
) : (
422-
<PlayCircle className="w-4 h-4" />
417+
<RefreshCw className="w-4 h-4" />
423418
)}
424-
Reactivate
419+
Retry Analysis
425420
</Button>
426-
);
427-
})()}
428-
</>
429-
)}
421+
)}
422+
423+
{(() => {
424+
const shouldShowReactivate = analysisData.status === ANALYSIS_STATUS.RUNNING && isAnalysisStale();
425+
console.log('Reactivate button check:', {
426+
status: analysisData.status,
427+
isRunning: analysisData.status === ANALYSIS_STATUS.RUNNING,
428+
isStale: isAnalysisStale(),
429+
shouldShow: shouldShowReactivate,
430+
ANALYSIS_STATUS
431+
});
432+
433+
return shouldShowReactivate && (
434+
<Button
435+
variant="outline"
436+
size="sm"
437+
onClick={handleReactivate}
438+
disabled={isRetrying}
439+
className="flex items-center gap-2"
440+
title={`Last updated ${formatDistanceToNow(new Date(analysisData.updated_at))} ago`}
441+
>
442+
{isRetrying ? (
443+
<Loader2 className="w-4 h-4 animate-spin" />
444+
) : (
445+
<PlayCircle className="w-4 h-4" />
446+
)}
447+
Reactivate
448+
</Button>
449+
);
450+
})()}
451+
</>
452+
)}
453+
454+
{/* Close button */}
455+
<Button
456+
size="sm"
457+
variant="outline"
458+
className="border border-slate-700"
459+
onClick={() => onClose()}
460+
>
461+
<X className="h-4 w-4" />
462+
</Button>
463+
</div>
430464
</div>
431465
<DialogDescription className="mt-2 flex justify-between items-center">
432466
<span>
@@ -630,7 +664,7 @@ export default function AnalysisDetailModal({ ticker, analysisId, isOpen, onClos
630664
</div>
631665
)}
632666
</div>
633-
</DialogContent>
667+
</DialogContentNoClose>
634668
</Dialog>
635669
);
636670
}

src/components/RebalanceDetailModal.tsx

Lines changed: 119 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { useState, useEffect, useRef } from "react";
1+
import React, { useState, useEffect, useRef } from "react";
22
import {
33
Dialog,
4-
DialogContent,
54
DialogDescription,
65
DialogHeader,
76
DialogTitle,
87
} from "@/components/ui/dialog";
8+
import * as DialogPrimitive from "@radix-ui/react-dialog";
9+
import { cn } from "@/lib/utils";
910
import { Badge } from "@/components/ui/badge";
11+
import { Button } from "@/components/ui/button";
1012
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
1113
import {
1214
RefreshCw,
@@ -17,7 +19,10 @@ import {
1719
Target,
1820
Brain,
1921
Activity,
20-
XCircle
22+
XCircle,
23+
X,
24+
ChevronUp,
25+
ChevronDown
2126
} from "lucide-react";
2227
import { formatDistanceToNow } from "date-fns";
2328
import { supabase } from "@/lib/supabase";
@@ -66,7 +71,26 @@ interface RebalancePosition {
6671
tradeActionId?: string;
6772
}
6873

69-
74+
// Custom DialogContent without the default close button
75+
const DialogContentNoClose = React.forwardRef<
76+
React.ElementRef<typeof DialogPrimitive.Content>,
77+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
78+
>(({ className, children, ...props }, ref) => (
79+
<DialogPrimitive.Portal>
80+
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
81+
<DialogPrimitive.Content
82+
ref={ref}
83+
className={cn(
84+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
85+
className
86+
)}
87+
{...props}
88+
>
89+
{children}
90+
</DialogPrimitive.Content>
91+
</DialogPrimitive.Portal>
92+
));
93+
DialogContentNoClose.displayName = "DialogContentNoClose";
7094

7195
export default function RebalanceDetailModal({ rebalanceId, isOpen, onClose, rebalanceDate }: RebalanceDetailModalProps) {
7296
const { user } = useAuth();
@@ -76,6 +100,7 @@ export default function RebalanceDetailModal({ rebalanceId, isOpen, onClose, reb
76100
const [loading, setLoading] = useState(true);
77101
const [error, setError] = useState<string | null>(null);
78102
const [isLiveRebalance, setIsLiveRebalance] = useState(false);
103+
const [collapsedCards, setCollapsedCards] = useState<Set<string>>(new Set());
79104
const intervalRef = useRef<NodeJS.Timeout | undefined>();
80105

81106
const [selectedAnalysis, setSelectedAnalysis] = useState<{
@@ -682,7 +707,7 @@ export default function RebalanceDetailModal({ rebalanceId, isOpen, onClose, reb
682707
return (
683708
<>
684709
<Dialog open={isOpen} onOpenChange={() => onClose()}>
685-
<DialogContent className="max-w-7xl max-h-[90vh] p-0">
710+
<DialogContentNoClose className="max-w-7xl max-h-[90vh] p-0">
686711
<DialogHeader className="px-6 pt-6 pb-4 border-b">
687712
<div className="flex items-center justify-between">
688713
<div className="flex items-center gap-3">
@@ -715,6 +740,16 @@ export default function RebalanceDetailModal({ rebalanceId, isOpen, onClose, reb
715740
</Badge>
716741
)}
717742
</div>
743+
744+
{/* Close button */}
745+
<Button
746+
size="sm"
747+
variant="outline"
748+
className="border border-slate-700"
749+
onClick={() => onClose()}
750+
>
751+
<X className="h-4 w-4" />
752+
</Button>
718753
</div>
719754
<DialogDescription className="mt-2 flex justify-between items-center">
720755
<span>
@@ -734,20 +769,82 @@ export default function RebalanceDetailModal({ rebalanceId, isOpen, onClose, reb
734769

735770
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1">
736771
<div className="px-6 pt-4 pb-4">
737-
<TabsList className="grid w-full grid-cols-3 max-w-3xl mx-auto">
738-
<TabsTrigger value="actions" className="flex items-center gap-2">
739-
<Target className="w-4 h-4" />
740-
Actions
741-
</TabsTrigger>
742-
<TabsTrigger value="workflow" className="flex items-center gap-2">
743-
<Activity className="w-4 h-4" />
744-
Workflow
745-
</TabsTrigger>
746-
<TabsTrigger value="insights" className="flex items-center gap-2">
747-
<Brain className="w-4 h-4" />
748-
Insights
749-
</TabsTrigger>
750-
</TabsList>
772+
<div className="relative flex items-center justify-center">
773+
<TabsList className="grid w-full grid-cols-3 max-w-3xl">
774+
<TabsTrigger value="actions" className="flex items-center gap-2">
775+
<Target className="w-4 h-4" />
776+
Actions
777+
</TabsTrigger>
778+
<TabsTrigger value="workflow" className="flex items-center gap-2">
779+
<Activity className="w-4 h-4" />
780+
Workflow
781+
</TabsTrigger>
782+
<TabsTrigger value="insights" className="flex items-center gap-2">
783+
<Brain className="w-4 h-4" />
784+
Insights
785+
</TabsTrigger>
786+
</TabsList>
787+
{activeTab === "insights" && (
788+
<Button
789+
variant="outline"
790+
size="sm"
791+
onClick={() => {
792+
if (collapsedCards.size === 0) {
793+
// Collapse all - need to get all card keys
794+
const allCardKeys = new Set<string>();
795+
796+
// Add threshold card if it exists
797+
if (!rebalanceData?.skipThresholdCheck) {
798+
const thresholdStep = rebalanceData?.workflowSteps?.find((s: any) => s.id === 'threshold');
799+
if (thresholdStep?.insights) {
800+
allCardKeys.add('threshold');
801+
}
802+
}
803+
804+
// Add opportunity card if it exists
805+
if (!rebalanceData?.skipOpportunityAgent) {
806+
const opportunityStep = rebalanceData?.workflowSteps?.find((s: any) => s.id === 'opportunity');
807+
if (opportunityStep?.insights || opportunityStep?.data) {
808+
allCardKeys.add('opportunity');
809+
}
810+
}
811+
812+
// Add portfolio manager card if it exists
813+
const portfolioInsights =
814+
rebalanceData?.rebalance_plan?.portfolioManagerAnalysis ||
815+
rebalanceData?.rebalance_plan?.portfolioManagerInsights ||
816+
rebalanceData?.rebalance_plan?.rebalance_agent_insight ||
817+
rebalanceData?.rebalance_plan?.agentInsights?.portfolioManager ||
818+
rebalanceData?.rebalance_plan?.agentInsights?.rebalanceAgent ||
819+
rebalanceData?.agentInsights?.portfolioManager ||
820+
rebalanceData?.agentInsights?.rebalanceAgent;
821+
822+
if (portfolioInsights) {
823+
allCardKeys.add('portfolioManager');
824+
}
825+
826+
setCollapsedCards(allCardKeys);
827+
} else {
828+
// Expand all
829+
setCollapsedCards(new Set());
830+
}
831+
}}
832+
className="text-xs absolute right-0"
833+
>
834+
{collapsedCards.size === 0 ? (
835+
<>
836+
<ChevronUp className="h-3 w-3 mr-1" />
837+
Collapse All
838+
</>
839+
) : (
840+
<>
841+
<ChevronDown className="h-3 w-3 mr-1" />
842+
Expand All
843+
</>
844+
)}
845+
</Button>
846+
)}
847+
</div>
751848
</div>
752849

753850
{error ? (
@@ -790,11 +887,13 @@ export default function RebalanceDetailModal({ rebalanceId, isOpen, onClose, reb
790887
rebalanceData={rebalanceData}
791888
selectedAnalysis={selectedAnalysis}
792889
setSelectedAnalysis={setSelectedAnalysis}
890+
collapsedCards={collapsedCards}
891+
setCollapsedCards={setCollapsedCards}
793892
/>
794893
</>
795894
)}
796895
</Tabs>
797-
</DialogContent>
896+
</DialogContentNoClose>
798897
</Dialog>
799898

800899
{/* Analysis Detail Modal */}

src/components/RecentTrades.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export default function RecentTrades() {
138138
console.log('Alpaca order map:', alpacaOrderMap);
139139

140140
// Convert Alpaca orders to AIDecision format
141+
// Only include orders that have an associated analysis or rebalance request
141142
const decisions: AIDecision[] = recentOrders.map(order => {
142143
// Use filled quantity if available, otherwise use order quantity
143144
const quantity = parseFloat(order.filled_qty || order.qty || '0');
@@ -206,6 +207,9 @@ export default function RecentTrades() {
206207
});
207208

208209
return decision;
210+
}).filter(decision => {
211+
// Only include orders that have either an analysis or rebalance link
212+
return decision.analysisId || decision.rebalanceRequestId;
209213
});
210214

211215
// Add trade orders from database that don't have Alpaca orders yet

0 commit comments

Comments
 (0)