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:
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>
|
||||
|
||||
@@ -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 & 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"
|
||||
|
||||
Reference in new issue
Block a user