Improve dashboard responsive layouts (#805)

* Improve dashboard responsive layouts

* Improve proxy pools mobile layout

* Improve CLI tool card input responsiveness

---------

Co-authored-by: Delynn Assistant <zhen@dkzhen.org>
This commit is contained in:
Zhen
2026-05-01 16:34:07 +07:00
committed by GitHub
co-authored by Delynn Assistant
parent 9ca388972c
commit 0da61d8f7b
29 changed files with 561 additions and 464 deletions
@@ -8,22 +8,22 @@ const fmtCost = (n) => `$${(n || 0).toFixed(2)}`;
export default function OverviewCards({ stats }) {
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="px-4 py-3 flex flex-col gap-1">
<div className="grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-4 sm:gap-4">
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
<span className="text-text-muted text-sm uppercase font-semibold">Total Requests</span>
<span className="text-2xl font-bold">{fmt(stats.totalRequests)}</span>
<span className="truncate text-2xl font-bold">{fmt(stats.totalRequests)}</span>
</Card>
<Card className="px-4 py-3 flex flex-col gap-1">
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
<span className="text-text-muted text-sm uppercase font-semibold">Total Input Tokens</span>
<span className="text-2xl font-bold text-primary">{fmt(stats.totalPromptTokens)}</span>
<span className="truncate text-2xl font-bold text-primary">{fmt(stats.totalPromptTokens)}</span>
</Card>
<Card className="px-4 py-3 flex flex-col gap-1">
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
<span className="text-text-muted text-sm uppercase font-semibold">Output Tokens</span>
<span className="text-2xl font-bold text-success">{fmt(stats.totalCompletionTokens)}</span>
<span className="truncate text-2xl font-bold text-success">{fmt(stats.totalCompletionTokens)}</span>
</Card>
<Card className="px-4 py-3 flex flex-col gap-1">
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
<span className="text-text-muted text-sm uppercase font-semibold">Est. Cost</span>
<span className="text-2xl font-bold text-warning">~{fmtCost(stats.totalCost)}</span>
<span className="truncate text-2xl font-bold text-warning">~{fmtCost(stats.totalCost)}</span>
<span className="text-[10px] text-text-muted">Estimated, not actual billing</span>
</Card>
</div>
@@ -29,6 +29,7 @@ export default function ProviderLimits() {
const [proxyPools, setProxyPools] = useState([]);
const [providerFilter, setProviderFilter] = useState("all");
const [expiringFirst, setExpiringFirst] = useState(false);
const [providerMenuOpen, setProviderMenuOpen] = useState(false);
const intervalRef = useRef(null);
const countdownRef = useRef(null);
@@ -389,6 +390,7 @@ export default function ProviderLimits() {
});
const providerOptions = Array.from(new Set(filteredConnections.map((conn) => conn.provider))).sort();
const selectedProviderLabel = providerFilter === "all" ? "All providers" : providerFilter;
// Calculate summary stats
const totalProviders = sortedConnections.length;
@@ -431,8 +433,8 @@ export default function ProviderLimits() {
return (
<div className="space-y-6">
{/* Header Controls */}
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-3">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-3">
<h2 className="text-xl font-semibold text-text-primary">
Provider Limits
</h2>
@@ -441,31 +443,89 @@ export default function ProviderLimits() {
</span>
</div>
<div className="flex items-center gap-2">
<select
value={providerFilter}
onChange={(event) => setProviderFilter(event.target.value)}
className="h-10 rounded-lg border border-black/10 bg-transparent px-3 text-sm text-text-primary dark:border-white/10"
aria-label="Filter quota providers"
>
<option value="all">All providers</option>
{providerOptions.map((provider) => (
<option key={provider} value={provider}>{provider}</option>
))}
</select>
<div className="flex flex-wrap items-center gap-2">
<div className="relative">
<button
type="button"
onClick={() => setProviderMenuOpen((prev) => !prev)}
className="flex h-10 min-w-[116px] items-center justify-between gap-2 rounded-xl border border-black/10 bg-black/[0.02] px-3 text-sm text-text-primary transition-colors hover:bg-black/5 dark:border-white/10 dark:bg-white/[0.03] dark:hover:bg-white/10 sm:min-w-[180px]"
aria-haspopup="menu"
aria-expanded={providerMenuOpen}
title="Filter quota providers"
>
<span className="flex min-w-0 items-center gap-2">
{providerFilter === "all" ? (
<span className="material-symbols-outlined text-[20px] text-text-muted">apps</span>
) : (
<ProviderIcon
src={`/providers/${providerFilter}.png`}
alt={providerFilter}
size={22}
className="size-[22px] rounded-md object-contain"
fallbackText={providerFilter.slice(0, 2).toUpperCase()}
/>
)}
<span className="truncate capitalize hidden sm:inline">{selectedProviderLabel}</span>
</span>
<span className="material-symbols-outlined text-[18px] text-text-muted">expand_more</span>
</button>
{providerMenuOpen && (
<>
<button
type="button"
className="fixed inset-0 z-30 bg-transparent"
aria-label="Close provider filter"
onClick={() => setProviderMenuOpen(false)}
/>
<div className="absolute left-0 z-40 mt-2 w-64 overflow-hidden rounded-2xl border border-black/10 bg-surface/95 p-1.5 shadow-xl shadow-black/10 backdrop-blur dark:border-white/10 dark:bg-surface/95 sm:w-72">
<button
type="button"
onClick={() => { setProviderFilter("all"); setProviderMenuOpen(false); }}
className={`flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors ${providerFilter === "all" ? "bg-primary/10 text-primary" : "text-text-primary hover:bg-black/5 dark:hover:bg-white/10"}`}
>
<span className="material-symbols-outlined text-[22px]">apps</span>
<span className="font-medium">All providers</span>
{providerFilter === "all" && <span className="material-symbols-outlined ml-auto text-[20px]">check</span>}
</button>
<div className="my-1 h-px bg-black/10 dark:bg-white/10" />
<div className="max-h-72 overflow-y-auto pr-1">
{providerOptions.map((provider) => (
<button
key={provider}
type="button"
onClick={() => { setProviderFilter(provider); setProviderMenuOpen(false); }}
className={`flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors ${providerFilter === provider ? "bg-primary/10 text-primary" : "text-text-primary hover:bg-black/5 dark:hover:bg-white/10"}`}
>
<ProviderIcon
src={`/providers/${provider}.png`}
alt={provider}
size={24}
className="size-6 rounded-md object-contain"
fallbackText={provider.slice(0, 2).toUpperCase()}
/>
<span className="font-medium capitalize">{provider}</span>
{providerFilter === provider && <span className="material-symbols-outlined ml-auto text-[20px]">check</span>}
</button>
))}
</div>
</div>
</>
)}
</div>
<button
type="button"
onClick={() => setExpiringFirst((prev) => !prev)}
className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-sm transition-colors ${expiringFirst ? "border-amber-500/40 bg-amber-500/10 text-amber-500" : "border-black/10 text-text-primary hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/5"}`}
className={`flex shrink-0 items-center gap-1.5 rounded-lg border px-3 py-2 text-sm transition-colors ${expiringFirst ? "border-amber-500/40 bg-amber-500/10 text-amber-500" : "border-black/10 text-text-primary hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/5"}`}
title="Sort accounts by earliest quota reset time"
>
<span className="material-symbols-outlined text-[18px]">hourglass_top</span>
Expiring first
<span className="hidden sm:inline">Expiring first</span>
</button>
{/* Auto-refresh toggle */}
<button
onClick={() => setAutoRefresh((prev) => !prev)}
className="flex items-center gap-2 px-3 py-2 rounded-lg border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
className="flex shrink-0 items-center gap-2 rounded-lg border border-black/10 px-3 py-2 transition-colors hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/5"
title={autoRefresh ? "Disable auto-refresh" : "Enable auto-refresh"}
>
<span
@@ -475,7 +535,7 @@ export default function ProviderLimits() {
>
{autoRefresh ? "toggle_on" : "toggle_off"}
</span>
<span className="text-sm text-text-primary">Auto-refresh</span>
<span className="hidden text-sm text-text-primary sm:inline">Auto-refresh</span>
{autoRefresh && (
<span className="text-xs text-text-muted">({countdown}s)</span>
)}
@@ -221,7 +221,7 @@ export default function ProviderTopology({ providers = [], activeRequests = [],
}, []);
return (
<div className="w-full rounded-lg border border-border bg-bg-subtle/30" style={{ height: 480 }}>
<div className="h-[320px] w-full min-w-0 rounded-lg border border-border bg-bg-subtle/30 sm:h-[480px]">
{providers.length === 0 ? (
<div className="h-full flex items-center justify-center text-text-muted text-sm">
No providers connected
@@ -169,10 +169,10 @@ export default function RequestDetailsTab() {
};
return (
<div className="flex flex-col gap-6">
<div className="flex min-w-0 flex-col gap-6">
<Card padding="md">
<div className="flex flex-wrap gap-4">
<div className="flex flex-col gap-2">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div className="flex min-w-0 flex-col gap-2">
<label htmlFor="provider-filter" className="text-sm font-medium text-text-main">Provider</label>
<select
id="provider-filter"
@@ -181,7 +181,7 @@ export default function RequestDetailsTab() {
className={cn(
"h-9 px-3 rounded-lg border border-black/10 dark:border-white/10 bg-surface",
"text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20",
"cursor-pointer min-w-[150px]"
"w-full min-w-0 cursor-pointer"
)}
>
<option value="">All Providers</option>
@@ -193,7 +193,7 @@ export default function RequestDetailsTab() {
</select>
</div>
<div className="flex flex-col gap-2">
<div className="flex min-w-0 flex-col gap-2">
<label htmlFor="start-date-filter" className="text-sm font-medium text-text-main">Start Date</label>
<input
id="start-date-filter"
@@ -202,12 +202,12 @@ export default function RequestDetailsTab() {
onChange={(e) => setFilters({ ...filters, startDate: e.target.value })}
className={cn(
"h-9 px-3 rounded-lg border border-black/10 dark:border-white/10 bg-surface",
"text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20"
"w-full min-w-0 text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20"
)}
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex min-w-0 flex-col gap-2">
<label htmlFor="end-date-filter" className="text-sm font-medium text-text-main">End Date</label>
<input
id="end-date-filter"
@@ -216,17 +216,18 @@ export default function RequestDetailsTab() {
onChange={(e) => setFilters({ ...filters, endDate: e.target.value })}
className={cn(
"h-9 px-3 rounded-lg border border-black/10 dark:border-white/10 bg-surface",
"text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20"
"w-full min-w-0 text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20"
)}
/>
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-medium text-text-main opacity-0" aria-hidden="true">Clear</span>
<div className="flex min-w-0 flex-col gap-2 sm:col-span-2 lg:col-span-1">
<span className="hidden text-sm font-medium text-text-main opacity-0 lg:block" aria-hidden="true">Clear</span>
<Button
variant="ghost"
onClick={handleClearFilters}
disabled={!filters.provider && !filters.startDate && !filters.endDate}
className="w-full"
>
Clear Filters
</Button>
@@ -236,7 +237,7 @@ export default function RequestDetailsTab() {
<Card padding="none">
<div className="overflow-x-auto">
<table className="w-full">
<table className="w-full min-w-[880px]">
<thead>
<tr className="border-b border-black/5 dark:border-white/5">
<th className="text-left p-4 text-sm font-semibold text-text-main">Timestamp</th>
@@ -270,13 +271,13 @@ export default function RequestDetailsTab() {
key={`${detail.id}-${index}`}
className="border-b border-black/5 dark:border-white/5 last:border-b-0 hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors"
>
<td className="p-4 text-sm text-text-main">
<td className="whitespace-nowrap p-4 text-sm text-text-main">
{new Date(detail.timestamp).toLocaleString()}
</td>
<td className="p-4 text-sm text-text-main font-mono">
<td className="max-w-[260px] truncate p-4 font-mono text-sm text-text-main">
{detail.model}
</td>
<td className="p-4 text-sm text-text-main">
<td className="max-w-[180px] truncate p-4 text-sm text-text-main">
<span className="font-medium">
{getProviderName(detail.provider, providerNameCache)}
</span>
@@ -330,10 +331,10 @@ export default function RequestDetailsTab() {
>
{selectedDetail && (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="grid min-w-0 grid-cols-1 gap-4 text-sm sm:grid-cols-2">
<div>
<span className="text-text-muted">ID:</span>{" "}
<span className="text-text-main font-mono">{selectedDetail.id}</span>
<span className="break-all font-mono text-text-main">{selectedDetail.id}</span>
</div>
<div>
<span className="text-text-muted">Timestamp:</span>{" "}
@@ -378,14 +379,14 @@ export default function RequestDetailsTab() {
<div className="space-y-4">
<CollapsibleSection title="1. Client Request (Input)" defaultOpen={true} icon="input">
<pre className="bg-black/5 dark:bg-white/5 p-4 rounded-lg overflow-auto max-h-[300px] text-xs font-mono text-text-main border border-black/5 dark:border-white/5">
<pre className="max-h-[300px] max-w-full overflow-auto rounded-lg border border-black/5 bg-black/5 p-3 font-mono text-xs text-text-main dark:border-white/5 dark:bg-white/5 sm:p-4">
{JSON.stringify(selectedDetail.request, null, 2)}
</pre>
</CollapsibleSection>
{selectedDetail.providerRequest && (
<CollapsibleSection title="2. Provider Request (Translated)" icon="translate">
<pre className="bg-black/5 dark:bg-white/5 p-4 rounded-lg overflow-auto max-h-[300px] text-xs font-mono text-text-main border border-black/5 dark:border-white/5">
<pre className="max-h-[300px] max-w-full overflow-auto rounded-lg border border-black/5 bg-black/5 p-3 font-mono text-xs text-text-main dark:border-white/5 dark:bg-white/5 sm:p-4">
{JSON.stringify(selectedDetail.providerRequest, null, 2)}
</pre>
</CollapsibleSection>
@@ -393,7 +394,7 @@ export default function RequestDetailsTab() {
{selectedDetail.providerResponse && (
<CollapsibleSection title="3. Provider Response (Raw)" icon="data_object">
<pre className="bg-black/5 dark:bg-white/5 p-4 rounded-lg overflow-auto max-h-[300px] text-xs font-mono text-text-main border border-black/5 dark:border-white/5">
<pre className="max-h-[300px] max-w-full overflow-auto rounded-lg border border-black/5 bg-black/5 p-3 font-mono text-xs text-text-main dark:border-white/5 dark:bg-white/5 sm:p-4">
{typeof selectedDetail.providerResponse === 'object'
? JSON.stringify(selectedDetail.providerResponse, null, 2)
: selectedDetail.providerResponse
@@ -409,7 +410,7 @@ export default function RequestDetailsTab() {
<span className="material-symbols-outlined text-[16px]">psychology</span>
Thinking Process
</h4>
<pre className="bg-amber-50 dark:bg-amber-950/30 p-4 rounded-lg overflow-auto max-h-[200px] text-xs font-mono text-amber-900 dark:text-amber-100 border border-amber-200 dark:border-amber-800">
<pre className="max-h-[200px] max-w-full overflow-auto rounded-lg border border-amber-200 bg-amber-50 p-3 font-mono text-xs text-amber-900 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-100 sm:p-4">
{selectedDetail.response.thinking}
</pre>
</div>
@@ -418,7 +419,7 @@ export default function RequestDetailsTab() {
<h4 className="font-semibold text-text-main mb-2 text-xs uppercase tracking-wide opacity-70">
Content
</h4>
<pre className="bg-black/5 dark:bg-white/5 p-4 rounded-lg overflow-auto max-h-[300px] text-xs font-mono text-text-main border border-black/5 dark:border-white/5">
<pre className="max-h-[300px] max-w-full overflow-auto rounded-lg border border-black/5 bg-black/5 p-3 font-mono text-xs text-text-main dark:border-white/5 dark:bg-white/5 sm:p-4">
{selectedDetail.response?.content || "[No content]"}
</pre>
</CollapsibleSection>
@@ -49,8 +49,8 @@ export default function UsageChart({ period = "7d" }) {
const hasData = data.some((d) => d.tokens > 0 || d.cost > 0);
return (
<Card className="p-4 flex flex-col gap-3">
<div className="flex items-center gap-1 bg-bg-subtle rounded-lg p-1 border border-border self-start">
<Card className="flex min-w-0 flex-col gap-3 p-3 sm:p-4">
<div className="grid w-full grid-cols-2 items-center gap-1 rounded-lg border border-border bg-bg-subtle p-1 sm:w-auto sm:self-start">
<button
onClick={() => setViewMode("tokens")}
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${viewMode === "tokens" ? "bg-primary text-white shadow-sm" : "text-text-muted hover:text-text hover:bg-bg-hover"}`}
@@ -70,7 +70,7 @@ export default function UsageChart({ period = "7d" }) {
) : !hasData ? (
<div className="h-48 flex items-center justify-center text-text-muted text-sm">No data for this period</div>
) : (
<ResponsiveContainer width="100%" height={200}>
<ResponsiveContainer width="100%" height={220}>
<AreaChart data={data} margin={{ top: 4, right: 8, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="gradTokens" x1="0" y1="0" x2="0" y2="1">