mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-09 16:23:58 +00:00
fix(ui): resolve layout and theme issues in profile editor
- Fix overflow issues in main layout by removing pb-12 and adding min-h-0 - Fix code editor theme switching by adding key prop to force re-render - Fix localhost disclaimer positioning by removing fixed positioning and redundant padding - Simplify analytics page layout structure and adjust spacing
This commit is contained in:
+1
-1
@@ -50,7 +50,7 @@ function Layout() {
|
|||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="flex-1 overflow-auto pb-12">
|
<div className="flex-1 overflow-auto min-h-0">
|
||||||
<Suspense fallback={<PageLoader />}>
|
<Suspense fallback={<PageLoader />}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export function CodeEditor({
|
|||||||
value={value}
|
value={value}
|
||||||
onValueChange={readonly ? () => {} : onChange}
|
onValueChange={readonly ? () => {} : onChange}
|
||||||
highlight={highlightCode}
|
highlight={highlightCode}
|
||||||
|
key={isDark ? 'dark' : 'light'}
|
||||||
padding={12}
|
padding={12}
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function LocalhostDisclaimer() {
|
|||||||
if (dismissed) return null;
|
if (dismissed) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-0 left-0 right-0 bg-yellow-50 dark:bg-yellow-900/20 border-t border-yellow-200 dark:border-yellow-800 px-4 py-2 z-50 md:pl-[var(--sidebar-width)] transition-[padding] duration-200">
|
<div className="w-full bg-yellow-50 dark:bg-yellow-900/20 border-t border-yellow-200 dark:border-yellow-800 px-4 py-2 transition-colors duration-200">
|
||||||
<div className="flex items-center justify-center gap-4">
|
<div className="flex items-center justify-center gap-4">
|
||||||
<div className="flex items-center gap-2 text-sm text-yellow-800 dark:text-yellow-200">
|
<div className="flex items-center gap-2 text-sm text-yellow-800 dark:text-yellow-200">
|
||||||
<Shield className="w-4 h-4 flex-shrink-0" />
|
<Shield className="w-4 h-4 flex-shrink-0" />
|
||||||
|
|||||||
+210
-212
@@ -96,230 +96,228 @@ export function AnalyticsPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-[calc(100vh-3rem)] overflow-hidden">
|
<div className="flex flex-col h-[calc(100vh-5.5rem)] overflow-hidden p-4 gap-3">
|
||||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden p-4 pb-14 space-y-4">
|
{/* Header */}
|
||||||
{/* Header */}
|
<div className="flex items-center justify-between shrink-0">
|
||||||
<div className="flex items-center justify-between shrink-0">
|
<div>
|
||||||
<div>
|
<h1 className="text-xl font-semibold">Analytics</h1>
|
||||||
<h1 className="text-xl font-semibold">Analytics</h1>
|
<p className="text-sm text-muted-foreground">Track usage & insights</p>
|
||||||
<p className="text-sm text-muted-foreground">Track usage & insights</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<DateRangeFilter
|
|
||||||
value={dateRange}
|
|
||||||
onChange={setDateRange}
|
|
||||||
presets={[
|
|
||||||
{ label: '7D', range: { from: subDays(new Date(), 7), to: new Date() } },
|
|
||||||
{ label: '30D', range: { from: subDays(new Date(), 30), to: new Date() } },
|
|
||||||
{ label: 'Month', range: { from: startOfMonth(new Date()), to: new Date() } },
|
|
||||||
{ label: 'All Time', range: { from: undefined, to: new Date() } },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
{lastUpdatedText && (
|
|
||||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
|
||||||
Updated {lastUpdatedText}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="gap-2 h-8"
|
|
||||||
onClick={handleRefresh}
|
|
||||||
disabled={isRefreshing}
|
|
||||||
>
|
|
||||||
<RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''}`} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DateRangeFilter
|
||||||
|
value={dateRange}
|
||||||
|
onChange={setDateRange}
|
||||||
|
presets={[
|
||||||
|
{ label: '7D', range: { from: subDays(new Date(), 7), to: new Date() } },
|
||||||
|
{ label: '30D', range: { from: subDays(new Date(), 30), to: new Date() } },
|
||||||
|
{ label: 'Month', range: { from: startOfMonth(new Date()), to: new Date() } },
|
||||||
|
{ label: 'All Time', range: { from: undefined, to: new Date() } },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{lastUpdatedText && (
|
||||||
|
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
Updated {lastUpdatedText}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 h-8"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Summary Cards */}
|
{/* Summary Cards */}
|
||||||
<UsageSummaryCards data={summary} isLoading={isSummaryLoading} />
|
<UsageSummaryCards data={summary} isLoading={isSummaryLoading} />
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 flex flex-col min-h-0 gap-3">
|
<div className="flex-1 flex flex-col min-h-0 gap-3">
|
||||||
{/* Usage Trend Chart - Full Width */}
|
{/* Usage Trend Chart - Full Width */}
|
||||||
<Card className="flex flex-col flex-1 min-h-0 shadow-sm">
|
<Card className="flex flex-col flex-1 min-h-0 shadow-sm">
|
||||||
|
<CardHeader className="px-3 py-2">
|
||||||
|
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
||||||
|
<TrendingUp className="w-4 h-4" />
|
||||||
|
Usage Trends
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-3 pb-3 pt-0 flex-1 min-h-0 flex items-center justify-center">
|
||||||
|
<UsageTrendChart data={trends || []} isLoading={isTrendsLoading} className="h-full" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Bottom Row - Cost by Model (4) + Model Usage (2) + Session Stats (4) */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-10 gap-3 flex-1 min-h-0">
|
||||||
|
{/* Cost by Model - 4/10 width with breakdown */}
|
||||||
|
<Card className="flex flex-col h-full min-h-0 shadow-sm lg:col-span-4">
|
||||||
<CardHeader className="px-3 py-2">
|
<CardHeader className="px-3 py-2">
|
||||||
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
||||||
<TrendingUp className="w-4 h-4" />
|
<DollarSign className="w-4 h-4" />
|
||||||
Usage Trends
|
Cost by Model
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-3 pb-3 pt-0 flex-1 min-h-0 flex items-center justify-center">
|
<CardContent className="px-2 pb-2 pt-0 flex-1 min-h-0 overflow-y-auto">
|
||||||
<UsageTrendChart data={trends || []} isLoading={isTrendsLoading} className="h-full" />
|
{isModelsLoading ? (
|
||||||
|
<Skeleton className="h-full w-full" />
|
||||||
|
) : (
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{[...(models || [])]
|
||||||
|
.sort((a, b) => b.cost - a.cost)
|
||||||
|
.map((model) => (
|
||||||
|
<button
|
||||||
|
key={model.model}
|
||||||
|
className="group flex items-center text-xs w-full hover:bg-muted/50 rounded px-2 py-1.5 transition-colors cursor-pointer gap-3"
|
||||||
|
onClick={(e) => handleModelClick(model, e)}
|
||||||
|
title="Click for details"
|
||||||
|
>
|
||||||
|
{/* Model name */}
|
||||||
|
<div className="flex items-center gap-2 min-w-0 w-[180px] shrink-0">
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 rounded-full shrink-0"
|
||||||
|
style={{ backgroundColor: getModelColor(model.model) }}
|
||||||
|
/>
|
||||||
|
<span className="font-medium truncate group-hover:underline underline-offset-2">
|
||||||
|
{model.model}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Cost breakdown mini-bar */}
|
||||||
|
<div className="flex-1 flex items-center gap-1 min-w-0">
|
||||||
|
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden flex">
|
||||||
|
<div
|
||||||
|
className="h-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#335c67',
|
||||||
|
width: `${model.cost > 0 ? (model.costBreakdown.input.cost / model.cost) * 100 : 0}%`,
|
||||||
|
}}
|
||||||
|
title={`Input: $${model.costBreakdown.input.cost.toFixed(2)}`}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="h-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#fff3b0',
|
||||||
|
width: `${model.cost > 0 ? (model.costBreakdown.output.cost / model.cost) * 100 : 0}%`,
|
||||||
|
}}
|
||||||
|
title={`Output: $${model.costBreakdown.output.cost.toFixed(2)}`}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="h-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#e09f3e',
|
||||||
|
width: `${model.cost > 0 ? (model.costBreakdown.cacheCreation.cost / model.cost) * 100 : 0}%`,
|
||||||
|
}}
|
||||||
|
title={`Cache Write: $${model.costBreakdown.cacheCreation.cost.toFixed(2)}`}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="h-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#9e2a2b',
|
||||||
|
width: `${model.cost > 0 ? (model.costBreakdown.cacheRead.cost / model.cost) * 100 : 0}%`,
|
||||||
|
}}
|
||||||
|
title={`Cache Read: $${model.costBreakdown.cacheRead.cost.toFixed(2)}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Token count */}
|
||||||
|
<span className="text-[10px] text-muted-foreground w-14 text-right shrink-0">
|
||||||
|
{formatTokens(model.tokens)}
|
||||||
|
</span>
|
||||||
|
{/* Total cost */}
|
||||||
|
<span className="font-mono font-medium w-16 text-right shrink-0">
|
||||||
|
${model.cost.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
<ChevronRight className="w-3 h-3 opacity-0 group-hover:opacity-50 transition-opacity shrink-0" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{/* Legend */}
|
||||||
|
<div className="flex items-center gap-3 pt-2 px-2 text-[10px] text-muted-foreground border-t mt-2">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 rounded-full"
|
||||||
|
style={{ backgroundColor: '#335c67' }}
|
||||||
|
/>
|
||||||
|
Input
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 rounded-full border border-muted-foreground/30"
|
||||||
|
style={{ backgroundColor: '#fff3b0' }}
|
||||||
|
/>
|
||||||
|
Output
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 rounded-full"
|
||||||
|
style={{ backgroundColor: '#e09f3e' }}
|
||||||
|
/>
|
||||||
|
Cache Write
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 rounded-full"
|
||||||
|
style={{ backgroundColor: '#9e2a2b' }}
|
||||||
|
/>
|
||||||
|
Cache Read
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Bottom Row - Cost by Model (4) + Model Usage (2) + Session Stats (4) */}
|
{/* Model Distribution - 2/10 width */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-10 gap-3 flex-1 min-h-0">
|
<Card className="flex flex-col h-full min-h-0 shadow-sm lg:col-span-2">
|
||||||
{/* Cost by Model - 4/10 width with breakdown */}
|
<CardHeader className="px-3 py-2">
|
||||||
<Card className="flex flex-col h-full min-h-0 shadow-sm lg:col-span-4">
|
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
||||||
<CardHeader className="px-3 py-2">
|
<PieChart className="w-4 h-4" />
|
||||||
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
Model Usage
|
||||||
<DollarSign className="w-4 h-4" />
|
</CardTitle>
|
||||||
Cost by Model
|
</CardHeader>
|
||||||
</CardTitle>
|
<CardContent className="px-2 pb-2 pt-0 flex-1 min-h-0 flex items-center justify-center">
|
||||||
</CardHeader>
|
<ModelBreakdownChart
|
||||||
<CardContent className="px-2 pb-2 pt-0 flex-1 min-h-0 overflow-y-auto">
|
data={models || []}
|
||||||
{isModelsLoading ? (
|
isLoading={isModelsLoading}
|
||||||
<Skeleton className="h-full w-full" />
|
className="h-full w-full"
|
||||||
) : (
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
{[...(models || [])]
|
|
||||||
.sort((a, b) => b.cost - a.cost)
|
|
||||||
.map((model) => (
|
|
||||||
<button
|
|
||||||
key={model.model}
|
|
||||||
className="group flex items-center text-xs w-full hover:bg-muted/50 rounded px-2 py-1.5 transition-colors cursor-pointer gap-3"
|
|
||||||
onClick={(e) => handleModelClick(model, e)}
|
|
||||||
title="Click for details"
|
|
||||||
>
|
|
||||||
{/* Model name */}
|
|
||||||
<div className="flex items-center gap-2 min-w-0 w-[180px] shrink-0">
|
|
||||||
<div
|
|
||||||
className="w-2 h-2 rounded-full shrink-0"
|
|
||||||
style={{ backgroundColor: getModelColor(model.model) }}
|
|
||||||
/>
|
|
||||||
<span className="font-medium truncate group-hover:underline underline-offset-2">
|
|
||||||
{model.model}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/* Cost breakdown mini-bar */}
|
|
||||||
<div className="flex-1 flex items-center gap-1 min-w-0">
|
|
||||||
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden flex">
|
|
||||||
<div
|
|
||||||
className="h-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: '#335c67',
|
|
||||||
width: `${model.cost > 0 ? (model.costBreakdown.input.cost / model.cost) * 100 : 0}%`,
|
|
||||||
}}
|
|
||||||
title={`Input: $${model.costBreakdown.input.cost.toFixed(2)}`}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="h-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: '#fff3b0',
|
|
||||||
width: `${model.cost > 0 ? (model.costBreakdown.output.cost / model.cost) * 100 : 0}%`,
|
|
||||||
}}
|
|
||||||
title={`Output: $${model.costBreakdown.output.cost.toFixed(2)}`}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="h-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: '#e09f3e',
|
|
||||||
width: `${model.cost > 0 ? (model.costBreakdown.cacheCreation.cost / model.cost) * 100 : 0}%`,
|
|
||||||
}}
|
|
||||||
title={`Cache Write: $${model.costBreakdown.cacheCreation.cost.toFixed(2)}`}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="h-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: '#9e2a2b',
|
|
||||||
width: `${model.cost > 0 ? (model.costBreakdown.cacheRead.cost / model.cost) * 100 : 0}%`,
|
|
||||||
}}
|
|
||||||
title={`Cache Read: $${model.costBreakdown.cacheRead.cost.toFixed(2)}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Token count */}
|
|
||||||
<span className="text-[10px] text-muted-foreground w-14 text-right shrink-0">
|
|
||||||
{formatTokens(model.tokens)}
|
|
||||||
</span>
|
|
||||||
{/* Total cost */}
|
|
||||||
<span className="font-mono font-medium w-16 text-right shrink-0">
|
|
||||||
${model.cost.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
<ChevronRight className="w-3 h-3 opacity-0 group-hover:opacity-50 transition-opacity shrink-0" />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
{/* Legend */}
|
|
||||||
<div className="flex items-center gap-3 pt-2 px-2 text-[10px] text-muted-foreground border-t mt-2">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<div
|
|
||||||
className="w-2 h-2 rounded-full"
|
|
||||||
style={{ backgroundColor: '#335c67' }}
|
|
||||||
/>
|
|
||||||
Input
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<div
|
|
||||||
className="w-2 h-2 rounded-full border border-muted-foreground/30"
|
|
||||||
style={{ backgroundColor: '#fff3b0' }}
|
|
||||||
/>
|
|
||||||
Output
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<div
|
|
||||||
className="w-2 h-2 rounded-full"
|
|
||||||
style={{ backgroundColor: '#e09f3e' }}
|
|
||||||
/>
|
|
||||||
Cache Write
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<div
|
|
||||||
className="w-2 h-2 rounded-full"
|
|
||||||
style={{ backgroundColor: '#9e2a2b' }}
|
|
||||||
/>
|
|
||||||
Cache Read
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Model Distribution - 2/10 width */}
|
|
||||||
<Card className="flex flex-col h-full min-h-0 shadow-sm lg:col-span-2">
|
|
||||||
<CardHeader className="px-3 py-2">
|
|
||||||
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
|
||||||
<PieChart className="w-4 h-4" />
|
|
||||||
Model Usage
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="px-2 pb-2 pt-0 flex-1 min-h-0 flex items-center justify-center">
|
|
||||||
<ModelBreakdownChart
|
|
||||||
data={models || []}
|
|
||||||
isLoading={isModelsLoading}
|
|
||||||
className="h-full w-full"
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Session Stats - 2/10 width */}
|
|
||||||
<SessionStatsCard
|
|
||||||
data={sessions}
|
|
||||||
isLoading={isSessionsLoading}
|
|
||||||
className="lg:col-span-2"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Usage Insights - 2/10 width */}
|
|
||||||
<UsageInsightsCard
|
|
||||||
anomalies={insights?.anomalies}
|
|
||||||
summary={insights?.summary}
|
|
||||||
isLoading={isInsightsLoading}
|
|
||||||
className="lg:col-span-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Model Details Popover - positioned at cursor */}
|
|
||||||
<Popover open={!!selectedModel} onOpenChange={(open) => !open && handlePopoverClose()}>
|
|
||||||
<PopoverAnchor asChild>
|
|
||||||
<div
|
|
||||||
ref={popoverAnchorRef}
|
|
||||||
className="fixed pointer-events-none"
|
|
||||||
style={{
|
|
||||||
left: popoverPosition?.x ?? 0,
|
|
||||||
top: popoverPosition?.y ?? 0,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</PopoverAnchor>
|
</CardContent>
|
||||||
<PopoverContent className="w-80 p-3" side="top" align="center">
|
</Card>
|
||||||
{selectedModel && <ModelDetailsContent model={selectedModel} />}
|
|
||||||
</PopoverContent>
|
{/* Session Stats - 2/10 width */}
|
||||||
</Popover>
|
<SessionStatsCard
|
||||||
|
data={sessions}
|
||||||
|
isLoading={isSessionsLoading}
|
||||||
|
className="lg:col-span-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Usage Insights - 2/10 width */}
|
||||||
|
<UsageInsightsCard
|
||||||
|
anomalies={insights?.anomalies}
|
||||||
|
summary={insights?.summary}
|
||||||
|
isLoading={isInsightsLoading}
|
||||||
|
className="lg:col-span-2"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Model Details Popover - positioned at cursor */}
|
||||||
|
<Popover open={!!selectedModel} onOpenChange={(open) => !open && handlePopoverClose()}>
|
||||||
|
<PopoverAnchor asChild>
|
||||||
|
<div
|
||||||
|
ref={popoverAnchorRef}
|
||||||
|
className="fixed pointer-events-none"
|
||||||
|
style={{
|
||||||
|
left: popoverPosition?.x ?? 0,
|
||||||
|
top: popoverPosition?.y ?? 0,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverAnchor>
|
||||||
|
<PopoverContent className="w-80 p-3" side="top" align="center">
|
||||||
|
{selectedModel && <ModelDetailsContent model={selectedModel} />}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user