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:
ZhenandDelynn Assistant authored and GitHub committed 2026-05-01 16:34:07 +07:00
1 parent 9ca388972c
commit 0da61d8f7b
29 files changed
+561 -464

No files matched your search

+61 -54
View File
@@ -245,19 +245,19 @@ export default function ProvidersPage() {
}
return (
<div className="flex flex-col gap-6">
<div className="flex min-w-0 flex-col gap-6 px-1 sm:px-0">
{/* OAuth Providers */}
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold flex items-center gap-2">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
OAuth Providers
</h2>
<div className="flex items-center gap-2">
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
<ModelAvailabilityBadge />
<button
onClick={() => handleBatchTest("oauth")}
disabled={!!testingMode}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
className={`flex w-full items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition-colors sm:w-auto sm:py-1.5 ${
testingMode === "oauth"
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
@@ -274,7 +274,7 @@ export default function ProvidersPage() {
</button>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
{Object.entries(OAUTH_PROVIDERS).map(([key, info]) => (
<ProviderCard
key={key}
@@ -288,16 +288,16 @@ export default function ProvidersPage() {
</div>
</div>
{/* Free & Free Tier Providers */}
{/* Free Tier Providers */}
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold flex items-center gap-2">
Free &amp; Free Tier Providers
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
Free Tier Providers
</h2>
<button
onClick={() => handleBatchTest("free")}
disabled={!!testingMode}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
className={`flex w-full items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition-colors sm:w-auto sm:py-1.5 ${
testingMode === "free"
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
@@ -313,7 +313,7 @@ export default function ProvidersPage() {
{testingMode === "free" ? "Testing..." : "Test All"}
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
{Object.entries(FREE_PROVIDERS).map(([key, info]) => (
<ProviderCard
key={key}
@@ -339,14 +339,14 @@ export default function ProvidersPage() {
{/* API Key Providers — fixed list */}
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold flex items-center gap-2">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
API Key Providers{" "}
</h2>
<button
onClick={() => handleBatchTest("apikey")}
disabled={!!testingMode}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
className={`flex w-full items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition-colors sm:w-auto sm:py-1.5 ${
testingMode === "apikey"
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
@@ -362,7 +362,7 @@ export default function ProvidersPage() {
{testingMode === "apikey" ? "Testing..." : "Test All"}
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
{Object.entries(APIKEY_PROVIDERS)
.filter(([, info]) => (info.serviceKinds ?? ["llm"]).includes("llm"))
.map(([key, info]) => (
@@ -401,11 +401,11 @@ export default function ProvidersPage() {
{/* API Key Compatible Providers — dynamic (OpenAI/Anthropic compatible) */}
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold flex items-center gap-2">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
API Key Compatible Providers{" "}
</h2>
<div className="flex gap-2">
<div className="grid grid-cols-1 gap-2 sm:flex sm:w-auto">
{/* {(compatibleProviders.length > 0 || anthropicCompatibleProviders.length > 0) && (
<button
onClick={() => handleBatchTest("compatible")}
@@ -426,6 +426,7 @@ export default function ProvidersPage() {
size="sm"
icon="add"
onClick={() => setShowAddAnthropicCompatibleModal(true)}
className="w-full sm:w-auto"
>
Add Anthropic Compatible
</Button>
@@ -434,7 +435,7 @@ export default function ProvidersPage() {
variant="secondary"
icon="add"
onClick={() => setShowAddCompatibleModal(true)}
className="!bg-white !text-black hover:!bg-gray-100"
className="w-full !bg-white !text-black hover:!bg-gray-100 sm:w-auto"
>
Add OpenAI Compatible
</Button>
@@ -455,7 +456,7 @@ export default function ProvidersPage() {
</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
{[...compatibleProviders, ...anthropicCompatibleProviders].map(
(info) => (
<ApiKeyProviderCard
@@ -494,12 +495,12 @@ export default function ProvidersPage() {
{/* Test Results Modal */}
{testResults && (
<div
className="fixed inset-0 z-50 flex items-start justify-center pt-[10vh]"
className="fixed inset-0 z-50 flex items-start justify-center px-3 pt-[6vh] sm:pt-[10vh]"
onClick={() => setTestResults(null)}
>
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
<div
className="relative bg-surface border border-border rounded-xl w-full max-w-[600px] max-h-[80vh] overflow-y-auto shadow-2xl"
className="relative bg-surface border border-border rounded-xl w-full max-w-[600px] max-h-[86vh] sm:max-h-[80vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="sticky top-0 z-10 flex items-center justify-between px-5 py-3 border-b border-border bg-surface/95 backdrop-blur-sm rounded-t-xl">
@@ -540,15 +541,15 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
};
return (
<Link href={`/dashboard/providers/${providerId}`} className="group">
<Link href={`/dashboard/providers/${providerId}`} className="group min-w-0">
<Card
padding="xs"
className={`h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer ${allDisabled ? "opacity-50" : ""}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex min-w-0 items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
<div
className="size-8 rounded-lg flex items-center justify-center"
className="size-8 shrink-0 rounded-lg flex items-center justify-center"
style={{
backgroundColor: `${provider.color?.length > 7 ? provider.color : provider.color + "15"}`,
}}
@@ -564,9 +565,9 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
fallbackColor={provider.color}
/>
</div>
<div>
<h3 className="font-semibold">{provider.name}</h3>
<div className="flex items-center gap-2 text-xs flex-wrap">
<div className="min-w-0">
<h3 className="truncate font-semibold">{provider.name}</h3>
<div className="flex min-w-0 items-center gap-1.5 text-xs flex-wrap">
{allDisabled ? (
<Badge variant="default" size="sm">
<span className="flex items-center gap-1">
@@ -589,10 +590,10 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
</div>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex shrink-0 items-center gap-2">
{stats.total > 0 && (
<div
className="opacity-0 group-hover:opacity-100 transition-opacity"
className="opacity-100 transition-opacity sm:opacity-0 sm:group-hover:opacity-100"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -668,15 +669,15 @@ function ApiKeyProviderCard({
};
return (
<Link href={`/dashboard/providers/${providerId}`} className="group">
<Link href={`/dashboard/providers/${providerId}`} className="group min-w-0">
<Card
padding="xs"
className={`h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer ${allDisabled ? "opacity-50" : ""}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex min-w-0 items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
<div
className="size-8 rounded-lg flex items-center justify-center"
className="size-8 shrink-0 rounded-lg flex items-center justify-center"
style={{
backgroundColor: `${provider.color?.length > 7 ? provider.color : provider.color + "15"}`,
}}
@@ -692,9 +693,9 @@ function ApiKeyProviderCard({
fallbackColor={provider.color}
/>
</div>
<div>
<h3 className="font-semibold">{provider.name}</h3>
<div className="flex items-center gap-2 text-xs flex-wrap">
<div className="min-w-0">
<h3 className="truncate font-semibold">{provider.name}</h3>
<div className="flex min-w-0 items-center gap-1.5 text-xs flex-wrap">
{allDisabled ? (
<Badge variant="default" size="sm">
<span className="flex items-center gap-1">
@@ -727,10 +728,10 @@ function ApiKeyProviderCard({
</div>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex shrink-0 items-center gap-2">
{stats.total > 0 && (
<div
className="opacity-0 group-hover:opacity-100 transition-opacity"
className="opacity-100 transition-opacity sm:opacity-0 sm:group-hover:opacity-100"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -927,17 +928,18 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
placeholder="e.g. gpt-4, claude-3-opus"
hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead."
/>
<div className="flex items-center gap-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<Button
onClick={handleValidate}
disabled={!checkKey || validating || !formData.baseUrl.trim()}
variant="secondary"
className="w-full sm:w-auto"
>
{validating ? "Checking..." : "Check"}
</Button>
{renderValidationResult()}
</div>
<div className="flex gap-2">
<div className="flex flex-col gap-2 sm:flex-row">
<Button
onClick={handleSubmit}
fullWidth
@@ -1108,17 +1110,18 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
placeholder="e.g. claude-3-opus"
hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead."
/>
<div className="flex items-center gap-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<Button
onClick={handleValidate}
disabled={!checkKey || validating || !formData.baseUrl.trim()}
variant="secondary"
className="w-full sm:w-auto"
>
{validating ? "Checking..." : "Check"}
</Button>
{renderValidationResult()}
</div>
<div className="flex gap-2">
<div className="flex flex-col gap-2 sm:flex-row">
<Button
onClick={handleSubmit}
fullWidth
@@ -1170,9 +1173,9 @@ function ProviderTestResultsView({ results }) {
}[mode] || mode;
return (
<div className="flex flex-col gap-3">
<div className="flex min-w-0 flex-col gap-3">
{summary && (
<div className="flex items-center gap-3 text-xs mb-1">
<div className="flex flex-wrap items-center gap-2 text-xs mb-1 sm:gap-3">
<span className="text-text-muted">{modeLabel} Test</span>
<span className="px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-400 font-medium">
{summary.passed} passed
@@ -1182,7 +1185,7 @@ function ProviderTestResultsView({ results }) {
{summary.failed} failed
</span>
)}
<span className="text-text-muted ml-auto">
<span className="text-text-muted sm:ml-auto">
{summary.total} tested
</span>
</div>
@@ -1190,24 +1193,28 @@ function ProviderTestResultsView({ results }) {
{items.map((r, i) => (
<div
key={r.connectionId || i}
className="flex items-center gap-2 text-xs px-3 py-2 rounded-lg bg-black/[0.03] dark:bg-white/[0.03]"
className="flex min-w-0 flex-wrap items-center gap-2 rounded-lg bg-black/[0.03] px-3 py-2 text-xs dark:bg-white/[0.03] sm:flex-nowrap"
>
<span
className={`material-symbols-outlined text-[16px] ${r.valid ? "text-emerald-500" : "text-red-500"}`}
>
{r.valid ? "check_circle" : "error"}
</span>
<div className="flex-1 min-w-0">
<span className="font-medium">{r.connectionName}</span>
<span className="text-text-muted ml-1.5">({r.provider})</span>
<div className="min-w-0 flex-[1_1_160px]">
<span className="block truncate font-medium sm:inline">
{r.connectionName}
</span>
<span className="block truncate text-text-muted sm:ml-1.5 sm:inline">
({r.provider})
</span>
</div>
{r.latencyMs !== undefined && (
<span className="text-text-muted font-mono tabular-nums">
<span className="shrink-0 text-text-muted font-mono tabular-nums">
{r.latencyMs}ms
</span>
)}
<span
className={`text-[10px] uppercase font-bold px-1.5 py-0.5 rounded ${
className={`shrink-0 text-[10px] uppercase font-bold px-1.5 py-0.5 rounded ${
r.valid
? "bg-emerald-500/15 text-emerald-400"
: "bg-red-500/15 text-red-400"