mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-05-03 05:20:36 +00:00
feat: update dashboard page and dashboard flow component
(cherry picked from commit 4bacc346d95d41bd89af4a27fffe566dbf1d8d4b)
This commit is contained in:
@@ -32,6 +32,35 @@
|
||||
rel="manifest"
|
||||
href="/favicon/site.webmanifest"
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
href="/fonts/inter-regular.woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
href="/fonts/inter-500.woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
href="/fonts/inter-600.woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
href="/fonts/inter-700.woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body class="bg-background font-sans antialiased">
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { ResponsiveContainer } from 'recharts';
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
export const ChartCard = ({
|
||||
children,
|
||||
className,
|
||||
description,
|
||||
height = 300,
|
||||
loading,
|
||||
title,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
description?: ReactNode;
|
||||
height?: number;
|
||||
loading?: boolean;
|
||||
title: ReactNode;
|
||||
}) => (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
style={{ height }}
|
||||
>
|
||||
<Loader2 className="text-muted-foreground size-6 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<ResponsiveContainer
|
||||
height={height}
|
||||
width="100%"
|
||||
>
|
||||
{children}
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -0,0 +1,48 @@
|
||||
import { formatNumber } from '@/lib/utils/format';
|
||||
|
||||
export type ChartTooltipPayloadEntry = {
|
||||
color: string;
|
||||
name: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export const ChartTooltip = ({
|
||||
active,
|
||||
formatter,
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
}: {
|
||||
active?: boolean;
|
||||
formatter?: (value: number, name: string) => string;
|
||||
label?: string;
|
||||
labelFormatter?: (label: string) => string;
|
||||
payload?: Array<ChartTooltipPayloadEntry>;
|
||||
}) => {
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderedLabel = label ? (labelFormatter ? labelFormatter(label) : label) : '';
|
||||
|
||||
return (
|
||||
<div className="bg-popover text-popover-foreground rounded-lg border px-3 py-2 shadow-md">
|
||||
{renderedLabel && <p className="text-muted-foreground mb-1 text-xs">{renderedLabel}</p>}
|
||||
{payload.map((entry) => (
|
||||
<div
|
||||
className="flex items-center gap-2 text-sm"
|
||||
key={entry.name}
|
||||
>
|
||||
<span
|
||||
className="size-2 rounded-full"
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
<span className="text-muted-foreground">{entry.name}:</span>
|
||||
<span className="font-medium">
|
||||
{formatter ? formatter(entry.value, entry.name) : formatNumber(entry.value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export { ChartCard } from './chart-card';
|
||||
export { ChartTooltip, type ChartTooltipPayloadEntry } from './chart-tooltip';
|
||||
export { MetricCard } from './metric-card';
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
export const MetricCard = ({
|
||||
className,
|
||||
description,
|
||||
icon,
|
||||
loading,
|
||||
title,
|
||||
value,
|
||||
}: {
|
||||
className?: string;
|
||||
description?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
loading?: boolean;
|
||||
title: ReactNode;
|
||||
value: ReactNode;
|
||||
}) => (
|
||||
<Card className={className}>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">{loading ? <Skeleton className="h-5 w-24" /> : title}</CardTitle>
|
||||
{loading ? <Skeleton className="size-4 shrink-0 rounded" /> : icon}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-1">
|
||||
{loading ? <Skeleton className="h-8 w-24" /> : <div className="text-2xl font-bold">{value}</div>}
|
||||
{description &&
|
||||
(loading ? (
|
||||
<Skeleton className="mt-1 h-3 w-32" />
|
||||
) : (
|
||||
<p className="text-muted-foreground text-xs">{description}</p>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { FlowStatusIcon } from '@/components/icons/flow-status-icon';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { StatusType } from '@/graphql/types';
|
||||
|
||||
const STATUS_LABELS: Record<StatusType, string> = {
|
||||
[StatusType.Created]: 'Created',
|
||||
[StatusType.Failed]: 'Failed',
|
||||
[StatusType.Finished]: 'Finished',
|
||||
[StatusType.Running]: 'Running',
|
||||
[StatusType.Waiting]: 'Waiting',
|
||||
};
|
||||
|
||||
export const FlowStatusBadge = ({ className, status }: { className?: string; status: StatusType }) => (
|
||||
<Badge
|
||||
className={className}
|
||||
variant="outline"
|
||||
>
|
||||
<FlowStatusIcon
|
||||
className="size-3"
|
||||
status={status}
|
||||
/>
|
||||
{STATUS_LABELS[status]}
|
||||
</Badge>
|
||||
);
|
||||
@@ -11,10 +11,17 @@ const badgeVariants = cva(
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
blue: 'border-blue-500/20 bg-blue-500/10 text-blue-600',
|
||||
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
green: 'border-green-500/20 bg-green-500/10 text-green-600',
|
||||
orange: 'border-orange-500/20 bg-orange-500/10 text-orange-600',
|
||||
outline: 'text-foreground',
|
||||
pink: 'border-pink-500/20 bg-pink-500/10 text-pink-600',
|
||||
purple: 'border-purple-500/20 bg-purple-500/10 text-purple-600',
|
||||
red: 'border-red-500/20 bg-red-500/10 text-red-600',
|
||||
secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
yellow: 'border-yellow-500/20 bg-yellow-500/10 text-yellow-600',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -22,6 +29,8 @@ const badgeVariants = cva(
|
||||
|
||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||
|
||||
export type BadgeVariant = NonNullable<VariantProps<typeof badgeVariants>['variant']>;
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -3,8 +3,9 @@ import { useMemo } from 'react';
|
||||
|
||||
import type { UsageStatsFragmentFragment } from '@/graphql/types';
|
||||
|
||||
import { MetricCard } from '@/components/dashboard';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import {
|
||||
useFlowStatsByFlowQuery,
|
||||
@@ -13,32 +14,7 @@ import {
|
||||
useUsageStatsByAgentTypeForFlowQuery,
|
||||
useUsageStatsByFlowQuery,
|
||||
} from '@/graphql/types';
|
||||
import { formatCost, formatDuration, formatNumber, formatTokenCount } from '@/pages/dashboard/format-utils';
|
||||
|
||||
const StatCard = ({
|
||||
description,
|
||||
icon,
|
||||
loading,
|
||||
title,
|
||||
value,
|
||||
}: {
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
loading: boolean;
|
||||
title: string;
|
||||
value: string;
|
||||
}) => (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
{icon}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? <Skeleton className="h-8 w-24" /> : <div className="text-2xl font-bold">{value}</div>}
|
||||
<p className="text-muted-foreground text-xs">{description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
import { formatCost, formatDuration, formatNumber, formatTokenCount } from '@/lib/utils/format';
|
||||
|
||||
const UsageStatsRow = ({ label, stats }: { label: string; stats: UsageStatsFragmentFragment }) => (
|
||||
<TableRow>
|
||||
@@ -126,33 +102,33 @@ export const FlowDashboardOverview = ({ flowId }: { flowId: string }) => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
<StatCard
|
||||
description="LLM spending for this flow"
|
||||
icon={<CircleDollarSign className="text-muted-foreground size-4" />}
|
||||
<MetricCard
|
||||
description={`Subtasks: ${flowStats?.totalSubtasksCount ?? 0} · Assistants: ${flowStats?.totalAssistantsCount ?? 0}`}
|
||||
icon={<GitFork className="text-muted-foreground size-4" />}
|
||||
loading={anyLoading}
|
||||
title="Cost"
|
||||
value={formatCost(totalCost)}
|
||||
title="Tasks"
|
||||
value={flowStats ? formatNumber(flowStats.totalTasksCount) : '0'}
|
||||
/>
|
||||
<StatCard
|
||||
description="Input + Output tokens"
|
||||
icon={<Cpu className="text-muted-foreground size-4" />}
|
||||
loading={anyLoading}
|
||||
title="Tokens"
|
||||
value={formatTokenCount(totalTokens)}
|
||||
/>
|
||||
<StatCard
|
||||
<MetricCard
|
||||
description={`Duration: ${toolcalls ? formatDuration(toolcalls.totalDurationSeconds) : '—'}`}
|
||||
icon={<Activity className="text-muted-foreground size-4" />}
|
||||
loading={anyLoading}
|
||||
title="Tool Calls"
|
||||
value={toolcalls ? formatNumber(toolcalls.totalCount) : '0'}
|
||||
/>
|
||||
<StatCard
|
||||
description={`Subtasks: ${flowStats?.totalSubtasksCount ?? 0} · Assistants: ${flowStats?.totalAssistantsCount ?? 0}`}
|
||||
icon={<GitFork className="text-muted-foreground size-4" />}
|
||||
<MetricCard
|
||||
description="Input + Output tokens"
|
||||
icon={<Cpu className="text-muted-foreground size-4" />}
|
||||
loading={anyLoading}
|
||||
title="Tasks"
|
||||
value={flowStats ? formatNumber(flowStats.totalTasksCount) : '0'}
|
||||
title="Tokens"
|
||||
value={formatTokenCount(totalTokens)}
|
||||
/>
|
||||
<MetricCard
|
||||
description="LLM spending for this flow"
|
||||
icon={<CircleDollarSign className="text-muted-foreground size-4" />}
|
||||
loading={anyLoading}
|
||||
title="Cost"
|
||||
value={formatCost(totalCost)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -219,15 +195,9 @@ export const FlowDashboardOverview = ({ flowId }: { flowId: string }) => {
|
||||
<TableRow key={item.functionName}>
|
||||
<TableCell className="font-medium">{item.functionName}</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
item.isAgent
|
||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
|
||||
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Badge variant={item.isAgent ? 'secondary' : 'outline'}>
|
||||
{item.isAgent ? 'Agent' : 'Tool'}
|
||||
</span>
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatNumber(item.totalCount)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const STORAGE_KEY_SEPARATOR = '_4_';
|
||||
|
||||
export type LocalStorageKeyType = 'column' | 'page' | 'sorting';
|
||||
export type LocalStorageKeyType = 'column' | 'page' | 'period' | 'sorting';
|
||||
|
||||
export function getColumnStorageKey(urlPath?: string): string {
|
||||
return getStorageKey('column', urlPath);
|
||||
@@ -10,6 +10,10 @@ export function getPageStorageKey(urlPath?: string): string {
|
||||
return getStorageKey('page', urlPath);
|
||||
}
|
||||
|
||||
export function getPeriodStorageKey(urlPath?: string): string {
|
||||
return getStorageKey('period', urlPath);
|
||||
}
|
||||
|
||||
export function getSortingStorageKey(urlPath?: string): string {
|
||||
return getStorageKey('sorting', urlPath);
|
||||
}
|
||||
|
||||
@@ -18,3 +18,59 @@ export const formatDate = (date: Date) => {
|
||||
|
||||
return format(date, 'HH:mm, d MMM yyyy', { locale: enUS });
|
||||
};
|
||||
|
||||
export const formatNumber = (value: number): string => new Intl.NumberFormat('en-US').format(value);
|
||||
|
||||
export const formatTokenCount = (count: number): string => {
|
||||
if (count >= 1_000_000_000) {
|
||||
return `${(count / 1_000_000_000).toFixed(1)}B`;
|
||||
}
|
||||
|
||||
if (count >= 1_000_000) {
|
||||
return `${(count / 1_000_000).toFixed(1)}M`;
|
||||
}
|
||||
|
||||
if (count >= 1_000) {
|
||||
return `${(count / 1_000).toFixed(1)}K`;
|
||||
}
|
||||
|
||||
return count.toString();
|
||||
};
|
||||
|
||||
export const formatCost = (cost: number): string => {
|
||||
if (!cost) {
|
||||
return '$0';
|
||||
}
|
||||
|
||||
if (cost >= 1) {
|
||||
return `$${cost.toFixed(2)}`;
|
||||
}
|
||||
|
||||
if (cost >= 0.01) {
|
||||
return `$${cost.toFixed(3)}`;
|
||||
}
|
||||
|
||||
return `$${cost.toFixed(4)}`;
|
||||
};
|
||||
|
||||
export const formatDuration = (seconds: number): string => {
|
||||
if (seconds >= 3600) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
if (seconds >= 60) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
|
||||
if (seconds >= 1) {
|
||||
return `${seconds.toFixed(1)}s`;
|
||||
}
|
||||
|
||||
return `${(seconds * 1000).toFixed(0)}ms`;
|
||||
};
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { format } from 'date-fns';
|
||||
import { ChevronRight, Clock, Loader2, Wrench } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Area, AreaChart, Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Area, AreaChart, Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import type { UsageStatsPeriod } from '@/graphql/types';
|
||||
import type { FlowFragmentFragment, UsageStatsPeriod } from '@/graphql/types';
|
||||
|
||||
import { ChartCard, ChartTooltip } from '@/components/dashboard';
|
||||
import { FlowStatusBadge } from '@/components/icons/flow-status-badge';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import {
|
||||
useFlowsExecutionStatsByPeriodQuery,
|
||||
useFlowsQuery,
|
||||
useFlowsStatsByPeriodQuery,
|
||||
useToolcallsStatsByPeriodQuery,
|
||||
useUsageStatsByPeriodQuery,
|
||||
} from '@/graphql/types';
|
||||
import { formatCost, formatDuration, formatNumber, formatTokenCount } from '@/pages/dashboard/format-utils';
|
||||
import { formatCost, formatDuration, formatNumber, formatTokenCount } from '@/lib/utils/format';
|
||||
|
||||
const CHART_COLORS = {
|
||||
area1: 'var(--color-chart-1)',
|
||||
@@ -31,48 +35,7 @@ const formatDateLabel = (dateString: string): string => {
|
||||
}
|
||||
};
|
||||
|
||||
const ChartLoading = () => (
|
||||
<div className="flex h-[300px] items-center justify-center">
|
||||
<Loader2 className="text-muted-foreground size-6 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const CustomTooltip = ({
|
||||
active,
|
||||
formatter,
|
||||
label,
|
||||
payload,
|
||||
}: {
|
||||
active?: boolean;
|
||||
formatter?: (value: number, name: string) => string;
|
||||
label?: string;
|
||||
payload?: Array<{ color: string; name: string; value: number }>;
|
||||
}) => {
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-popover text-popover-foreground rounded-lg border px-3 py-2 shadow-md">
|
||||
<p className="text-muted-foreground mb-1 text-xs">{label ? formatDateLabel(label) : ''}</p>
|
||||
{payload.map((entry) => (
|
||||
<div
|
||||
className="flex items-center gap-2 text-sm"
|
||||
key={entry.name}
|
||||
>
|
||||
<span
|
||||
className="size-2 rounded-full"
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
<span className="text-muted-foreground">{entry.name}:</span>
|
||||
<span className="font-medium">
|
||||
{formatter ? formatter(entry.value, entry.name) : formatNumber(entry.value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const axisTickStyle = { fill: 'var(--color-muted-foreground)', fontSize: 12 };
|
||||
|
||||
export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) => {
|
||||
const { data: usageByPeriodData, loading: usageByPeriodLoading } = useUsageStatsByPeriodQuery({
|
||||
@@ -87,12 +50,22 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
|
||||
const { data: executionStatsData, loading: executionStatsLoading } = useFlowsExecutionStatsByPeriodQuery({
|
||||
variables: { period },
|
||||
});
|
||||
const { data: flowsData } = useFlowsQuery();
|
||||
|
||||
const flowsById = useMemo(() => {
|
||||
const map = new Map<string, FlowFragmentFragment>();
|
||||
(flowsData?.flows ?? []).forEach((flow) => {
|
||||
map.set(flow.id, flow);
|
||||
});
|
||||
|
||||
return map;
|
||||
}, [flowsData?.flows]);
|
||||
|
||||
const usageChartData = [...(usageByPeriodData?.usageStatsByPeriod ?? [])].reverse().map((item) => ({
|
||||
cacheIn: item.stats.totalUsageCacheIn,
|
||||
costIn: item.stats.totalUsageCostIn,
|
||||
costOut: item.stats.totalUsageCostOut,
|
||||
date: formatDateLabel(item.date),
|
||||
date: item.date,
|
||||
tokensIn: item.stats.totalUsageIn,
|
||||
tokensOut: item.stats.totalUsageOut,
|
||||
totalCost: item.stats.totalUsageCostIn + item.stats.totalUsageCostOut,
|
||||
@@ -100,13 +73,13 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
|
||||
|
||||
const toolcallsChartData = [...(toolcallsByPeriodData?.toolcallsStatsByPeriod ?? [])].reverse().map((item) => ({
|
||||
count: item.stats.totalCount,
|
||||
date: formatDateLabel(item.date),
|
||||
date: item.date,
|
||||
duration: item.stats.totalDurationSeconds,
|
||||
}));
|
||||
|
||||
const flowsChartData = [...(flowsByPeriodData?.flowsStatsByPeriod ?? [])].reverse().map((item) => ({
|
||||
assistants: item.stats.totalAssistantsCount,
|
||||
date: formatDateLabel(item.date),
|
||||
date: item.date,
|
||||
flows: item.stats.totalFlowsCount,
|
||||
subtasks: item.stats.totalSubtasksCount,
|
||||
tasks: item.stats.totalTasksCount,
|
||||
@@ -116,210 +89,184 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<ChartCard
|
||||
description="Flows, tasks, and subtasks created per day"
|
||||
height={320}
|
||||
loading={flowsByPeriodLoading}
|
||||
title="Flows Activity Over Time"
|
||||
>
|
||||
<BarChart data={flowsChartData}>
|
||||
<CartesianGrid
|
||||
className="stroke-border"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tick={axisTickStyle}
|
||||
tickFormatter={formatDateLabel}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<YAxis
|
||||
tick={axisTickStyle}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<Tooltip
|
||||
content={<ChartTooltip labelFormatter={formatDateLabel} />}
|
||||
cursor={{ fill: 'var(--color-muted-foreground)', fillOpacity: 0.1 }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="flows"
|
||||
fill={CHART_COLORS.area1}
|
||||
name="Flows"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="tasks"
|
||||
fill={CHART_COLORS.area2}
|
||||
name="Tasks"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="subtasks"
|
||||
fill={CHART_COLORS.area3}
|
||||
name="Subtasks"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartCard>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Token Usage Over Time</CardTitle>
|
||||
<CardDescription>Input and output tokens processed daily</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{usageByPeriodLoading ? (
|
||||
<ChartLoading />
|
||||
) : (
|
||||
<ResponsiveContainer
|
||||
height={300}
|
||||
width="100%"
|
||||
>
|
||||
<AreaChart data={usageChartData}>
|
||||
<CartesianGrid
|
||||
className="stroke-border"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tick={{ fill: 'var(--color-muted-foreground)', fontSize: 12 }}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: 'var(--color-muted-foreground)', fontSize: 12 }}
|
||||
tickFormatter={formatTokenCount}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<Tooltip
|
||||
content={<CustomTooltip formatter={(value) => formatTokenCount(value)} />}
|
||||
/>
|
||||
<Area
|
||||
dataKey="tokensIn"
|
||||
fill={CHART_COLORS.area1}
|
||||
fillOpacity={0.3}
|
||||
name="Tokens In"
|
||||
stroke={CHART_COLORS.area1}
|
||||
type="monotone"
|
||||
/>
|
||||
<Area
|
||||
dataKey="tokensOut"
|
||||
fill={CHART_COLORS.area2}
|
||||
fillOpacity={0.3}
|
||||
name="Tokens Out"
|
||||
stroke={CHART_COLORS.area2}
|
||||
type="monotone"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ChartCard
|
||||
description="Number of tool executions per day"
|
||||
loading={toolcallsByPeriodLoading}
|
||||
title="Tool Calls Over Time"
|
||||
>
|
||||
<BarChart data={toolcallsChartData}>
|
||||
<CartesianGrid
|
||||
className="stroke-border"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tick={axisTickStyle}
|
||||
tickFormatter={formatDateLabel}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<YAxis
|
||||
tick={axisTickStyle}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<Tooltip
|
||||
content={<ChartTooltip labelFormatter={formatDateLabel} />}
|
||||
cursor={{ fill: 'var(--color-muted-foreground)', fillOpacity: 0.1 }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="count"
|
||||
fill={CHART_COLORS.bar1}
|
||||
name="Tool Calls"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartCard>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Cost Over Time</CardTitle>
|
||||
<CardDescription>LLM spending per day</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{usageByPeriodLoading ? (
|
||||
<ChartLoading />
|
||||
) : (
|
||||
<ResponsiveContainer
|
||||
height={300}
|
||||
width="100%"
|
||||
>
|
||||
<AreaChart data={usageChartData}>
|
||||
<CartesianGrid
|
||||
className="stroke-border"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tick={{ fill: 'var(--color-muted-foreground)', fontSize: 12 }}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: 'var(--color-muted-foreground)', fontSize: 12 }}
|
||||
tickFormatter={(value) => formatCost(value)}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip formatter={(value) => formatCost(value)} />} />
|
||||
<Area
|
||||
dataKey="costIn"
|
||||
fill={CHART_COLORS.area1}
|
||||
fillOpacity={0.3}
|
||||
name="Cost In"
|
||||
stroke={CHART_COLORS.area1}
|
||||
type="monotone"
|
||||
/>
|
||||
<Area
|
||||
dataKey="costOut"
|
||||
fill={CHART_COLORS.area3}
|
||||
fillOpacity={0.3}
|
||||
name="Cost Out"
|
||||
stroke={CHART_COLORS.area3}
|
||||
type="monotone"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Tool Calls Over Time</CardTitle>
|
||||
<CardDescription>Number of tool executions per day</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{toolcallsByPeriodLoading ? (
|
||||
<ChartLoading />
|
||||
) : (
|
||||
<ResponsiveContainer
|
||||
height={300}
|
||||
width="100%"
|
||||
>
|
||||
<BarChart data={toolcallsChartData}>
|
||||
<CartesianGrid
|
||||
className="stroke-border"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tick={{ fill: 'var(--color-muted-foreground)', fontSize: 12 }}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: 'var(--color-muted-foreground)', fontSize: 12 }}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<Tooltip
|
||||
content={<CustomTooltip />}
|
||||
cursor={{ fill: 'var(--color-muted-foreground)', fillOpacity: 0.1 }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="count"
|
||||
fill={CHART_COLORS.bar1}
|
||||
name="Tool Calls"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Flows Activity Over Time</CardTitle>
|
||||
<CardDescription>Flows, tasks, and subtasks created per day</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{flowsByPeriodLoading ? (
|
||||
<ChartLoading />
|
||||
) : (
|
||||
<ResponsiveContainer
|
||||
height={300}
|
||||
width="100%"
|
||||
>
|
||||
<BarChart data={flowsChartData}>
|
||||
<CartesianGrid
|
||||
className="stroke-border"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tick={{ fill: 'var(--color-muted-foreground)', fontSize: 12 }}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: 'var(--color-muted-foreground)', fontSize: 12 }}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<Tooltip
|
||||
content={<CustomTooltip />}
|
||||
cursor={{ fill: 'var(--color-muted-foreground)', fillOpacity: 0.1 }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="flows"
|
||||
fill={CHART_COLORS.area1}
|
||||
name="Flows"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="tasks"
|
||||
fill={CHART_COLORS.area2}
|
||||
name="Tasks"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="subtasks"
|
||||
fill={CHART_COLORS.area3}
|
||||
name="Subtasks"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ChartCard
|
||||
description="Input and output tokens processed daily"
|
||||
loading={usageByPeriodLoading}
|
||||
title="Token Usage Over Time"
|
||||
>
|
||||
<AreaChart data={usageChartData}>
|
||||
<CartesianGrid
|
||||
className="stroke-border"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tick={axisTickStyle}
|
||||
tickFormatter={formatDateLabel}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<YAxis
|
||||
tick={axisTickStyle}
|
||||
tickFormatter={formatTokenCount}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<Tooltip
|
||||
content={
|
||||
<ChartTooltip
|
||||
formatter={(value) => formatTokenCount(value)}
|
||||
labelFormatter={formatDateLabel}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Area
|
||||
dataKey="tokensIn"
|
||||
fill={CHART_COLORS.area1}
|
||||
fillOpacity={0.3}
|
||||
name="Tokens In"
|
||||
stroke={CHART_COLORS.area1}
|
||||
type="monotone"
|
||||
/>
|
||||
<Area
|
||||
dataKey="tokensOut"
|
||||
fill={CHART_COLORS.area2}
|
||||
fillOpacity={0.3}
|
||||
name="Tokens Out"
|
||||
stroke={CHART_COLORS.area2}
|
||||
type="monotone"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartCard>
|
||||
</div>
|
||||
|
||||
<ChartCard
|
||||
description="LLM spending per day. May stay near zero when using local engines — this is expected."
|
||||
height={240}
|
||||
loading={usageByPeriodLoading}
|
||||
title="Cost Over Time"
|
||||
>
|
||||
<AreaChart data={usageChartData}>
|
||||
<CartesianGrid
|
||||
className="stroke-border"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tick={axisTickStyle}
|
||||
tickFormatter={formatDateLabel}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<YAxis
|
||||
tick={axisTickStyle}
|
||||
tickFormatter={(value) => formatCost(value)}
|
||||
tickMargin={8}
|
||||
/>
|
||||
<Tooltip
|
||||
content={
|
||||
<ChartTooltip
|
||||
formatter={(value) => formatCost(value)}
|
||||
labelFormatter={formatDateLabel}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Area
|
||||
dataKey="costIn"
|
||||
fill={CHART_COLORS.area1}
|
||||
fillOpacity={0.3}
|
||||
name="Cost In"
|
||||
stroke={CHART_COLORS.area1}
|
||||
type="monotone"
|
||||
/>
|
||||
<Area
|
||||
dataKey="costOut"
|
||||
fill={CHART_COLORS.area3}
|
||||
fillOpacity={0.3}
|
||||
name="Cost Out"
|
||||
stroke={CHART_COLORS.area3}
|
||||
type="monotone"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartCard>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Flow Execution Details</CardTitle>
|
||||
@@ -339,6 +286,7 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
|
||||
{executionStats.map((flow) => (
|
||||
<FlowExecutionItem
|
||||
flow={flow}
|
||||
flowMeta={flowsById.get(flow.flowId)}
|
||||
key={flow.flowId}
|
||||
/>
|
||||
))}
|
||||
@@ -370,25 +318,39 @@ type FlowExecution = {
|
||||
totalToolcallsCount: number;
|
||||
};
|
||||
|
||||
const FlowExecutionItem = ({ flow }: { flow: FlowExecution }) => {
|
||||
const FlowExecutionItem = ({ flow, flowMeta }: { flow: FlowExecution; flowMeta?: FlowFragmentFragment }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const taskCount = flow.tasks.length;
|
||||
const subtaskCount = flow.tasks.reduce((sum, task) => sum + task.subtasks.length, 0);
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
onOpenChange={setIsOpen}
|
||||
open={isOpen}
|
||||
>
|
||||
<CollapsibleTrigger className="hover:bg-muted/50 flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors">
|
||||
<ChevronRight className={`size-4 shrink-0 transition-transform ${isOpen ? 'rotate-90' : ''}`} />
|
||||
<div className="flex-1 truncate font-medium">{flow.flowTitle || `Flow #${flow.flowId}`}</div>
|
||||
<div className="text-muted-foreground flex items-center gap-4 text-sm">
|
||||
<CollapsibleTrigger className="hover:bg-muted/50 group flex w-full items-start gap-3 rounded-lg px-3 py-2 text-left transition-colors">
|
||||
<ChevronRight className={`mt-1 size-4 shrink-0 transition-transform ${isOpen ? 'rotate-90' : ''}`} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="truncate font-medium">{flow.flowTitle || `Flow #${flow.flowId}`}</span>
|
||||
{flowMeta?.status && <FlowStatusBadge status={flowMeta.status} />}
|
||||
{flowMeta?.provider?.name && <Badge variant="secondary">{flowMeta.provider.name}</Badge>}
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-0.5 text-xs">
|
||||
{taskCount} {taskCount === 1 ? 'task' : 'tasks'}
|
||||
{subtaskCount > 0 && ` · ${subtaskCount} ${subtaskCount === 1 ? 'subtask' : 'subtasks'}`}
|
||||
{flow.totalAssistantsCount > 0 &&
|
||||
` · ${flow.totalAssistantsCount} ${flow.totalAssistantsCount === 1 ? 'assistant' : 'assistants'}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground flex shrink-0 items-center gap-4 pt-1 text-sm">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="size-3" />
|
||||
{formatDuration(flow.totalDurationSeconds)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Wrench className="size-3" />
|
||||
{flow.totalToolcallsCount}
|
||||
{formatNumber(flow.totalToolcallsCount)}
|
||||
</span>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
@@ -432,7 +394,7 @@ const TaskExecutionItem = ({ task }: { task: FlowExecution['tasks'][number] }) =
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Wrench className="size-3" />
|
||||
{task.totalToolcallsCount}
|
||||
{formatNumber(task.totalToolcallsCount)}
|
||||
</span>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
@@ -454,7 +416,7 @@ const TaskExecutionItem = ({ task }: { task: FlowExecution['tasks'][number] }) =
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Wrench className="size-3" />
|
||||
{subtask.totalToolcallsCount}
|
||||
{formatNumber(subtask.totalToolcallsCount)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,9 @@ import { Activity, CircleDollarSign, Cpu, GitFork, Loader2 } from 'lucide-react'
|
||||
|
||||
import type { UsageStatsFragmentFragment } from '@/graphql/types';
|
||||
|
||||
import { MetricCard } from '@/components/dashboard';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import {
|
||||
useFlowsStatsTotalQuery,
|
||||
@@ -14,32 +15,7 @@ import {
|
||||
useUsageStatsByProviderQuery,
|
||||
useUsageStatsTotalQuery,
|
||||
} from '@/graphql/types';
|
||||
import { formatCost, formatDuration, formatNumber, formatTokenCount } from '@/pages/dashboard/format-utils';
|
||||
|
||||
const StatCard = ({
|
||||
description,
|
||||
icon,
|
||||
loading,
|
||||
title,
|
||||
value,
|
||||
}: {
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
loading: boolean;
|
||||
title: string;
|
||||
value: string;
|
||||
}) => (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
{icon}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? <Skeleton className="h-8 w-24" /> : <div className="text-2xl font-bold">{value}</div>}
|
||||
<p className="text-muted-foreground text-xs">{description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
import { formatCost, formatDuration, formatNumber, formatTokenCount } from '@/lib/utils/format';
|
||||
|
||||
const UsageStatsRow = ({ label, stats }: { label: string; stats: UsageStatsFragmentFragment }) => (
|
||||
<TableRow>
|
||||
@@ -124,33 +100,33 @@ export const DashboardOverview = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard
|
||||
description="Total LLM spending across all providers"
|
||||
icon={<CircleDollarSign className="text-muted-foreground size-4" />}
|
||||
loading={usageTotalLoading}
|
||||
title="Total Cost"
|
||||
value={formatCost(totalCost)}
|
||||
<MetricCard
|
||||
description={`Tasks: ${flowsTotal?.totalTasksCount ?? 0} · Subtasks: ${flowsTotal?.totalSubtasksCount ?? 0} · Assistants: ${flowsTotal?.totalAssistantsCount ?? 0}`}
|
||||
icon={<GitFork className="text-muted-foreground size-4" />}
|
||||
loading={flowsTotalLoading}
|
||||
title="Total Flows"
|
||||
value={flowsTotal ? formatNumber(flowsTotal.totalFlowsCount) : '0'}
|
||||
/>
|
||||
<StatCard
|
||||
description="Input + Output tokens processed"
|
||||
icon={<Cpu className="text-muted-foreground size-4" />}
|
||||
loading={usageTotalLoading}
|
||||
title="Total Tokens"
|
||||
value={formatTokenCount(totalTokens)}
|
||||
/>
|
||||
<StatCard
|
||||
<MetricCard
|
||||
description={`Total duration: ${toolcallsTotal ? formatDuration(toolcallsTotal.totalDurationSeconds) : '—'}`}
|
||||
icon={<Activity className="text-muted-foreground size-4" />}
|
||||
loading={toolcallsTotalLoading}
|
||||
title="Tool Calls"
|
||||
value={toolcallsTotal ? formatNumber(toolcallsTotal.totalCount) : '0'}
|
||||
/>
|
||||
<StatCard
|
||||
description={`Tasks: ${flowsTotal?.totalTasksCount ?? 0} · Subtasks: ${flowsTotal?.totalSubtasksCount ?? 0} · Assistants: ${flowsTotal?.totalAssistantsCount ?? 0}`}
|
||||
icon={<GitFork className="text-muted-foreground size-4" />}
|
||||
loading={flowsTotalLoading}
|
||||
title="Total Flows"
|
||||
value={flowsTotal ? formatNumber(flowsTotal.totalFlowsCount) : '0'}
|
||||
<MetricCard
|
||||
description="Input + Output tokens processed"
|
||||
icon={<Cpu className="text-muted-foreground size-4" />}
|
||||
loading={usageTotalLoading}
|
||||
title="Total Tokens"
|
||||
value={formatTokenCount(totalTokens)}
|
||||
/>
|
||||
<MetricCard
|
||||
description="Total LLM spending across all providers"
|
||||
icon={<CircleDollarSign className="text-muted-foreground size-4" />}
|
||||
loading={usageTotalLoading}
|
||||
title="Total Cost"
|
||||
value={formatCost(totalCost)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -208,15 +184,9 @@ export const DashboardOverview = () => {
|
||||
<TableRow key={item.functionName}>
|
||||
<TableCell className="font-medium">{item.functionName}</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
item.isAgent
|
||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
|
||||
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Badge variant={item.isAgent ? 'secondary' : 'outline'}>
|
||||
{item.isAgent ? 'Agent' : 'Tool'}
|
||||
</span>
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(item.totalCount)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { LayoutDashboard } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from '@/components/ui/breadcrumb';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { UsageStatsPeriod } from '@/graphql/types';
|
||||
import { getPeriodStorageKey } from '@/lib/storage-keys';
|
||||
import { DashboardAnalytics } from '@/pages/dashboard/dashboard-analytics';
|
||||
import { DashboardOverview } from '@/pages/dashboard/dashboard-overview';
|
||||
|
||||
@@ -15,9 +16,40 @@ const periodOptions: { label: string; value: UsageStatsPeriod }[] = [
|
||||
{ label: 'Quarter', value: UsageStatsPeriod.Quarter },
|
||||
];
|
||||
|
||||
const VALID_PERIODS = new Set<string>(Object.values(UsageStatsPeriod));
|
||||
|
||||
const loadPeriod = (storageKey: string): UsageStatsPeriod => {
|
||||
try {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
|
||||
if (stored && VALID_PERIODS.has(stored)) {
|
||||
return stored as UsageStatsPeriod;
|
||||
}
|
||||
} catch {
|
||||
/* localStorage may be unavailable */
|
||||
}
|
||||
|
||||
return UsageStatsPeriod.Week;
|
||||
};
|
||||
|
||||
const savePeriod = (storageKey: string, value: UsageStatsPeriod): void => {
|
||||
try {
|
||||
localStorage.setItem(storageKey, value);
|
||||
} catch {
|
||||
/* localStorage may be unavailable */
|
||||
}
|
||||
};
|
||||
|
||||
const Dashboard = () => {
|
||||
const periodStorageKey = useMemo(() => getPeriodStorageKey(), []);
|
||||
const [activeTab, setActiveTab] = useState('analytics');
|
||||
const [period, setPeriod] = useState<UsageStatsPeriod>(UsageStatsPeriod.Week);
|
||||
const [period, setPeriod] = useState<UsageStatsPeriod>(() => loadPeriod(periodStorageKey));
|
||||
|
||||
const handlePeriodChange = (value: string) => {
|
||||
const next = value as UsageStatsPeriod;
|
||||
setPeriod(next);
|
||||
savePeriod(periodStorageKey, next);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -53,7 +85,7 @@ const Dashboard = () => {
|
||||
|
||||
{activeTab === 'analytics' && (
|
||||
<Tabs
|
||||
onValueChange={(value) => setPeriod(value as UsageStatsPeriod)}
|
||||
onValueChange={handlePeriodChange}
|
||||
value={period}
|
||||
>
|
||||
<TabsList>
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
export const formatTokenCount = (count: number): string => {
|
||||
if (count >= 1_000_000_000) {
|
||||
return `${(count / 1_000_000_000).toFixed(1)}B`;
|
||||
}
|
||||
|
||||
if (count >= 1_000_000) {
|
||||
return `${(count / 1_000_000).toFixed(1)}M`;
|
||||
}
|
||||
|
||||
if (count >= 1_000) {
|
||||
return `${(count / 1_000).toFixed(1)}K`;
|
||||
}
|
||||
|
||||
return count.toString();
|
||||
};
|
||||
|
||||
export const formatCost = (cost: number): string => {
|
||||
if (!cost) {
|
||||
return '$0';
|
||||
}
|
||||
|
||||
if (cost >= 1) {
|
||||
return `$${cost.toFixed(2)}`;
|
||||
}
|
||||
|
||||
if (cost >= 0.01) {
|
||||
return `$${cost.toFixed(3)}`;
|
||||
}
|
||||
|
||||
return `$${cost.toFixed(4)}`;
|
||||
};
|
||||
|
||||
export const formatDuration = (seconds: number): string => {
|
||||
if (seconds >= 3600) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
if (seconds >= 60) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
|
||||
if (seconds >= 1) {
|
||||
return `${seconds.toFixed(1)}s`;
|
||||
}
|
||||
|
||||
return `${(seconds * 1000).toFixed(0)}ms`;
|
||||
};
|
||||
|
||||
export const formatNumber = (value: number): string => {
|
||||
return new Intl.NumberFormat('en-US').format(value);
|
||||
};
|
||||
@@ -4,6 +4,15 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Fallback';
|
||||
src: local('Arial');
|
||||
ascent-override: 90%;
|
||||
descent-override: 22.43%;
|
||||
line-gap-override: 0%;
|
||||
size-adjust: 107.64%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
@@ -297,9 +306,9 @@
|
||||
--sidebar-accent-foreground: oklch(0.32 0 0);
|
||||
--sidebar-border: oklch(0.93 0.01 240);
|
||||
--sidebar-ring: oklch(0.25 0.14 245);
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-serif: Inter, serif;
|
||||
--font-mono: Roboto Mono, monospace;
|
||||
--font-sans: Inter, 'Inter Fallback', sans-serif;
|
||||
--font-serif: Inter, 'Inter Fallback', serif;
|
||||
--font-mono: 'Roboto Mono', monospace;
|
||||
--radius: 0.375rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 1px;
|
||||
@@ -352,8 +361,8 @@
|
||||
--sidebar-accent-foreground: oklch(0.92 0.02 245);
|
||||
--sidebar-border: oklch(0.3 0.04 245);
|
||||
--sidebar-ring: oklch(0.5 0.16 245);
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-serif: Inter, serif;
|
||||
--font-sans: Inter, 'Inter Fallback', sans-serif;
|
||||
--font-serif: Inter, 'Inter Fallback', serif;
|
||||
--font-mono: 'Roboto Mono', monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0;
|
||||
|
||||
Reference in New Issue
Block a user