mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +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 />
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex-1 overflow-auto pb-12">
|
||||
<div className="flex-1 overflow-auto min-h-0">
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
|
||||
@@ -126,6 +126,7 @@ export function CodeEditor({
|
||||
value={value}
|
||||
onValueChange={readonly ? () => {} : onChange}
|
||||
highlight={highlightCode}
|
||||
key={isDark ? 'dark' : 'light'}
|
||||
padding={12}
|
||||
disabled={readonly}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
|
||||
@@ -7,7 +7,7 @@ export function LocalhostDisclaimer() {
|
||||
if (dismissed) return null;
|
||||
|
||||
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 gap-2 text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<Shield className="w-4 h-4 flex-shrink-0" />
|
||||
|
||||
+210
-212
@@ -96,230 +96,228 @@ export function AnalyticsPage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-[calc(100vh-3rem)] overflow-hidden">
|
||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden p-4 pb-14 space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between shrink-0">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">Analytics</h1>
|
||||
<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 className="flex flex-col h-[calc(100vh-5.5rem)] overflow-hidden p-4 gap-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between shrink-0">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">Analytics</h1>
|
||||
<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>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<UsageSummaryCards data={summary} isLoading={isSummaryLoading} />
|
||||
{/* Summary Cards */}
|
||||
<UsageSummaryCards data={summary} isLoading={isSummaryLoading} />
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col min-h-0 gap-3">
|
||||
{/* Usage Trend Chart - Full Width */}
|
||||
<Card className="flex flex-col flex-1 min-h-0 shadow-sm">
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col min-h-0 gap-3">
|
||||
{/* Usage Trend Chart - Full Width */}
|
||||
<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">
|
||||
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
Usage Trends
|
||||
<DollarSign className="w-4 h-4" />
|
||||
Cost by Model
|
||||
</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 className="px-2 pb-2 pt-0 flex-1 min-h-0 overflow-y-auto">
|
||||
{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>
|
||||
</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">
|
||||
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
||||
<DollarSign className="w-4 h-4" />
|
||||
Cost by Model
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 pb-2 pt-0 flex-1 min-h-0 overflow-y-auto">
|
||||
{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>
|
||||
</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,
|
||||
}}
|
||||
{/* 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"
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
<PopoverContent className="w-80 p-3" side="top" align="center">
|
||||
{selectedModel && <ModelDetailsContent model={selectedModel} />}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</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>
|
||||
<PopoverContent className="w-80 p-3" side="top" align="center">
|
||||
{selectedModel && <ModelDetailsContent model={selectedModel} />}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user