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

@@ -109,10 +109,10 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
};
return (
<div className={`group flex items-center justify-between p-2 rounded-lg hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors ${connection.isActive === false ? "opacity-60" : ""}`}>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className={`group flex min-w-0 flex-col gap-3 rounded-lg p-2 transition-colors hover:bg-black/[0.02] dark:hover:bg-white/[0.02] sm:flex-row sm:items-center sm:justify-between ${connection.isActive === false ? "opacity-60" : ""}`}>
<div className="flex min-w-0 flex-1 items-start gap-2 sm:items-center sm:gap-3">
{/* Priority arrows */}
<div className="flex flex-col">
<div className="flex shrink-0 flex-col">
<button
onClick={onMoveUp}
disabled={isFirst}
@@ -128,12 +128,12 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
<span className="material-symbols-outlined text-sm">keyboard_arrow_down</span>
</button>
</div>
<span className="material-symbols-outlined text-base text-text-muted">
<span className="material-symbols-outlined shrink-0 text-base text-text-muted">
{isOAuth ? "lock" : "key"}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{displayName}</p>
<div className="flex items-center gap-2 mt-1">
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-1.5 sm:gap-2">
<Badge variant={getStatusVariant()} size="sm" dot>
{connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")}
</Badge>
@@ -144,7 +144,7 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
)}
{isCooldown && connection.isActive !== false && <CooldownTimer until={modelLockUntil} />}
{connection.lastError && connection.isActive !== false && (
<span className="text-xs text-red-500 truncate max-w-[300px]" title={connection.lastError}>
<span className="max-w-full truncate text-xs text-red-500 sm:max-w-[300px]" title={connection.lastError}>
{connection.lastError}
</span>
)}
@@ -155,16 +155,16 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
</div>
{hasAnyProxy && (
<div className="mt-1 flex items-center gap-2 flex-wrap">
<span className="text-[11px] text-text-muted truncate max-w-[420px]" title={proxyDisplayText}>
<span className="max-w-full truncate text-[11px] text-text-muted sm:max-w-[420px]" title={proxyDisplayText}>
{proxyDisplayText}
</span>
{maskedProxyUrl && (
<code className="text-[10px] font-mono bg-black/5 dark:bg-white/5 px-1 py-0.5 rounded text-text-muted">
<code className="max-w-full truncate rounded bg-black/5 px-1 py-0.5 font-mono text-[10px] text-text-muted dark:bg-white/5 sm:max-w-[260px]">
{maskedProxyUrl}
</code>
)}
{noProxyText && (
<span className="text-[11px] text-text-muted truncate max-w-[320px]" title={noProxyText}>
<span className="max-w-full truncate text-[11px] text-text-muted sm:max-w-[320px]" title={noProxyText}>
no_proxy: {noProxyText}
</span>
)}
@@ -172,14 +172,14 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
)}
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex gap-1">
<div className="flex w-full items-center justify-between gap-2 sm:w-auto sm:justify-end">
<div className="grid flex-1 grid-cols-3 gap-1 sm:flex sm:flex-none">
{/* Proxy button with inline dropdown */}
{(proxyPools || []).length > 0 && (
<div className="relative" ref={proxyDropdownRef}>
<button
onClick={() => setShowProxyDropdown((v) => !v)}
className={`flex flex-col items-center px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/5 transition-colors ${hasAnyProxy ? "text-primary" : "text-text-muted hover:text-primary"}`}
className={`flex w-full flex-col items-center rounded px-2 py-1 transition-colors hover:bg-black/5 dark:hover:bg-white/5 ${hasAnyProxy ? "text-primary" : "text-text-muted hover:text-primary"}`}
disabled={updatingProxy}
>
<span className="material-symbols-outlined text-[18px]">
@@ -188,7 +188,7 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
<span className="text-[10px] leading-tight">Proxy</span>
</button>
{showProxyDropdown && (
<div className="absolute right-0 top-full mt-1 z-50 bg-bg border border-border rounded-lg shadow-lg py-1 min-w-[160px]">
<div className="absolute right-0 top-full z-50 mt-1 max-w-[78vw] min-w-[160px] rounded-lg border border-border bg-bg py-1 shadow-lg">
<button
onClick={() => handleSelectProxy("__none__")}
className={`w-full text-left px-3 py-1.5 text-sm hover:bg-black/5 dark:hover:bg-white/5 ${!boundProxyPoolId ? "text-primary font-medium" : "text-text-main"}`}
@@ -208,11 +208,11 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
)}
</div>
)}
<button onClick={onEdit} className="flex flex-col items-center px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary">
<button onClick={onEdit} className="flex flex-col items-center rounded px-2 py-1 text-text-muted hover:bg-black/5 hover:text-primary dark:hover:bg-white/5">
<span className="material-symbols-outlined text-[18px]">edit</span>
<span className="text-[10px] leading-tight">Edit</span>
</button>
<button onClick={onDelete} className="flex flex-col items-center px-2 py-1 rounded hover:bg-red-500/10 text-red-500">
<button onClick={onDelete} className="flex flex-col items-center rounded px-2 py-1 text-red-500 hover:bg-red-500/10">
<span className="material-symbols-outlined text-[18px]">delete</span>
<span className="text-[10px] leading-tight">Delete</span>
</button>
@@ -14,24 +14,24 @@ export default function ModelRow({ model, fullModel, alias, copied, onCopy, test
: undefined;
return (
<div className={`group px-3 py-2 rounded-lg border ${borderColor} hover:bg-sidebar/50`}>
<div className="flex items-center gap-2">
<div className={`group min-w-0 max-w-full rounded-lg border px-3 py-2 ${borderColor} hover:bg-sidebar/50`}>
<div className="flex min-w-0 items-start gap-2 sm:items-center">
<span
className="material-symbols-outlined text-base"
className="material-symbols-outlined shrink-0 text-base"
style={iconColor ? { color: iconColor } : undefined}
>
{testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"}
</span>
<div className="flex flex-col gap-1">
<code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">{fullModel}</code>
{model.name && <span className="text-[9px] text-text-muted/70 italic pl-1">{model.name}</span>}
<div className="flex min-w-0 flex-1 flex-col gap-1">
<code className="max-w-[72vw] truncate rounded bg-sidebar px-1.5 py-0.5 font-mono text-xs text-text-muted sm:max-w-[360px]">{fullModel}</code>
{model.name && <span className="truncate pl-1 text-[9px] italic text-text-muted/70">{model.name}</span>}
</div>
{onTest && (
<div className="relative group/btn">
<div className="relative shrink-0 group/btn">
<button
onClick={onTest}
disabled={isTesting}
className={`p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary transition-opacity ${isTesting ? "opacity-100" : "opacity-0 group-hover:opacity-100"}`}
className={`rounded p-0.5 text-text-muted transition-opacity hover:bg-sidebar hover:text-primary ${isTesting ? "opacity-100" : "opacity-100 sm:opacity-0 sm:group-hover:opacity-100"}`}
>
<span className="material-symbols-outlined text-sm" style={isTesting ? { animation: "spin 1s linear infinite" } : undefined}>
{isTesting ? "progress_activity" : "science"}
@@ -42,10 +42,10 @@ export default function ModelRow({ model, fullModel, alias, copied, onCopy, test
</span>
</div>
)}
<div className="relative group/btn">
<div className="relative shrink-0 group/btn">
<button
onClick={() => onCopy(fullModel, `model-${model.id}`)}
className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary"
className="rounded p-0.5 text-text-muted hover:bg-sidebar hover:text-primary"
>
<span className="material-symbols-outlined text-sm">
{copied === `model-${model.id}` ? "check" : "content_copy"}
@@ -58,7 +58,7 @@ export default function ModelRow({ model, fullModel, alias, copied, onCopy, test
{isCustom && (
<button
onClick={onDeleteAlias}
className="p-0.5 hover:bg-red-500/10 rounded text-text-muted hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity ml-auto"
className="ml-auto rounded p-0.5 text-text-muted opacity-100 transition-opacity hover:bg-red-500/10 hover:text-red-500 sm:opacity-0 sm:group-hover:opacity-100"
title="Remove custom model"
>
<span className="material-symbols-outlined text-sm">close</span>
@@ -464,10 +464,10 @@ export default function ProviderDetailPage() {
const isSelected = (connectionId) => selectedConnectionIds.includes(connectionId);
const connectionsList = (
<div className="flex flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
<div className="flex min-w-0 flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
{connections
.map((conn, index) => (
<div key={conn.id} className="flex items-stretch">
<div key={conn.id} className="flex min-w-0 items-stretch">
<div className="flex-1 min-w-0">
<ConnectionRow
connection={conn}
@@ -537,7 +537,7 @@ export default function ProviderDetailPage() {
<p className="text-xs text-text-muted">{bulkHint}</p>
<p className="text-xs text-text-muted">Selecting None will unbind selected connections from proxy pool.</p>
<div className="flex gap-2">
<div className="flex flex-col gap-2 sm:flex-row">
<Button onClick={handleBulkApplyProxyPool} fullWidth disabled={!canApplyBulkProxy}>
{bulkUpdatingProxy ? "Applying..." : "Apply"}
</Button>
@@ -656,7 +656,7 @@ export default function ProviderDetailPage() {
{/* Add model button — inline, same style as model chips */}
<button
onClick={() => setShowAddCustomModel(true)}
className="flex items-center gap-1.5 px-3 py-2 rounded-lg border border-dashed border-black/15 dark:border-white/15 text-xs text-text-muted hover:text-primary hover:border-primary/40 transition-colors"
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-black/15 px-3 py-2 text-xs text-text-muted transition-colors hover:border-primary/40 hover:text-primary sm:w-auto"
>
<span className="material-symbols-outlined text-sm">add</span>
Add Model
@@ -728,9 +728,9 @@ export default function ProviderDetailPage() {
};
return (
<div className="flex flex-col gap-8">
<div className="flex min-w-0 flex-col gap-6 px-1 sm:gap-8 sm:px-0">
{/* Header */}
<div>
<div className="min-w-0">
<Link
href="/dashboard/providers"
className="inline-flex items-center gap-1 text-sm text-text-muted hover:text-primary transition-colors mb-4"
@@ -738,9 +738,9 @@ export default function ProviderDetailPage() {
<span className="material-symbols-outlined text-lg">arrow_back</span>
Back to Providers
</Link>
<div className="flex items-center gap-4">
<div className="flex min-w-0 items-center gap-3 sm:gap-4">
<div
className="rounded-lg flex items-center justify-center"
className="flex size-12 shrink-0 items-center justify-center rounded-lg"
style={{ backgroundColor: `${providerInfo.color}15` }}
>
{headerImgError ? (
@@ -753,14 +753,14 @@ export default function ProviderDetailPage() {
alt={providerInfo.name}
width={48}
height={48}
className="object-contain rounded-lg max-w-[48px] max-h-[48px]"
className="max-h-12 max-w-12 rounded-lg object-contain"
sizes="48px"
onError={() => setHeaderImgError(true)}
/>
)}
</div>
<div>
<h1 className="text-3xl font-semibold tracking-tight">{providerInfo.name}</h1>
<div className="min-w-0">
<h1 className="truncate text-2xl font-semibold tracking-tight sm:text-3xl">{providerInfo.name}</h1>
<p className="text-text-muted">
{connections.length} connection{connections.length === 1 ? "" : "s"}
</p>
@@ -776,15 +776,15 @@ export default function ProviderDetailPage() {
)}
{providerInfo.notice && !providerInfo.deprecated && (
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-blue-500/10 border border-blue-500/30">
<div className="flex flex-col gap-2 rounded-lg border border-blue-500/30 bg-blue-500/10 px-3 py-2 sm:flex-row sm:items-center">
<span className="material-symbols-outlined text-[16px] text-blue-500 shrink-0">info</span>
<p className="text-xs text-blue-600 dark:text-blue-400 leading-relaxed">{providerInfo.notice.text}</p>
<p className="min-w-0 flex-1 text-xs leading-relaxed text-blue-600 dark:text-blue-400">{providerInfo.notice.text}</p>
{providerInfo.notice.apiKeyUrl && (
<a
href={providerInfo.notice.apiKeyUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-medium text-white bg-blue-500 hover:bg-blue-600 px-2 py-0.5 rounded shrink-0 transition-colors"
className="inline-flex justify-center rounded bg-blue-500 px-2 py-1 text-xs font-medium text-white transition-colors hover:bg-blue-600 sm:py-0.5"
>
Get API Key →
</a>
@@ -794,20 +794,21 @@ export default function ProviderDetailPage() {
{isCompatible && providerNode && (
<Card>
<div className="flex items-center justify-between mb-4">
<div>
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<h2 className="text-lg font-semibold">{isAnthropicCompatible ? "Anthropic Compatible Details" : "OpenAI Compatible Details"}</h2>
<p className="text-sm text-text-muted">
<p className="break-all text-sm text-text-muted">
{isAnthropicCompatible ? "Messages API" : (providerNode.apiType === "responses" ? "Responses API" : "Chat Completions")} · {(providerNode.baseUrl || "").replace(/\/$/, "")}/
{isAnthropicCompatible ? "messages" : (providerNode.apiType === "responses" ? "responses" : "chat/completions")}
</p>
</div>
<div className="flex items-center gap-2">
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
<Button
size="sm"
icon="add"
onClick={() => setShowAddApiKeyModal(true)}
disabled={connections.length > 0}
className="w-full sm:w-auto"
>
Add
</Button>
@@ -816,6 +817,7 @@ export default function ProviderDetailPage() {
variant="secondary"
icon="edit"
onClick={() => setShowEditNodeModal(true)}
className="w-full sm:w-auto"
>
Edit
</Button>
@@ -834,6 +836,7 @@ export default function ProviderDetailPage() {
console.log("Error deleting provider node:", error);
}
}}
className="w-full sm:w-auto"
>
Delete
</Button>
@@ -852,9 +855,9 @@ export default function ProviderDetailPage() {
<NoAuthProxyCard providerId={providerId} />
) : (
<Card>
<div className="flex items-center justify-between mb-4">
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg font-semibold">Connections</h2>
<div className="flex items-center gap-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
{/* Thinking config */}
{/* {thinkingConfig && (
<div className="flex items-center gap-2">
@@ -871,7 +874,7 @@ export default function ProviderDetailPage() {
</div>
)} */}
{/* Round Robin toggle */}
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs text-text-muted font-medium">Round Robin</span>
<Toggle
checked={providerStrategy === "round-robin"}
@@ -902,7 +905,7 @@ export default function ProviderDetailPage() {
<p className="text-text-main font-medium mb-1">No connections yet</p>
<p className="text-sm text-text-muted mb-4">Add your first connection to get started</p>
{!isCompatible && (
<div className="flex gap-2 justify-center">
<div className="flex flex-col gap-2 justify-center sm:flex-row">
{providerId === "iflow" && (
<Button icon="cookie" variant="secondary" onClick={() => setShowIFlowCookieModal(true)}>
Cookie Auth
@@ -918,7 +921,7 @@ export default function ProviderDetailPage() {
<>
{connectionsList}
{!isCompatible && (
<div className="flex gap-2 mt-4">
<div className="mt-4 grid grid-cols-1 gap-2 sm:flex">
{providerId === "iflow" && (
<Button
size="sm"
@@ -926,6 +929,7 @@ export default function ProviderDetailPage() {
variant="secondary"
onClick={() => setShowIFlowCookieModal(true)}
title="Add connection using browser cookie"
className="w-full sm:w-auto"
>
Cookie
</Button>
@@ -934,6 +938,7 @@ export default function ProviderDetailPage() {
size="sm"
icon="add"
onClick={() => isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true)}
className="w-full sm:w-auto"
>
Add
</Button>
@@ -946,7 +951,7 @@ export default function ProviderDetailPage() {
{/* Models */}
<Card>
<div className="flex items-center justify-between mb-4">
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg font-semibold">
{"Available Models"}
</h2>
+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"