Improve mobile layouts and restore Cloudflare provider (#840)

Co-authored-by: Delynn Assistant <zhen@dkzhen.org>
This commit is contained in:
ZhenandDelynn Assistant authored and GitHub committed 2026-05-03 14:55:43 +07:00
1 parent ef3abeacf1
commit 14ff538f2e
20 files changed
+223 -210

No files matched your search

@@ -16,9 +16,9 @@ import { getTtsVoicesForModel } from "open-sse/config/ttsModels.js";
// Shared row layout — defined outside components to avoid re-mount on re-render
function Row({ label, children }) {
return (
<div className="flex items-center gap-3">
<span className="text-xs text-text-muted w-20 shrink-0">{label}</span>
<div className="flex-1">{children}</div>
<div className="flex min-w-0 flex-col gap-1.5 sm:flex-row sm:items-center sm:gap-3">
<span className="w-full text-xs font-medium text-text-muted sm:w-20 sm:shrink-0">{label}</span>
<div className="w-full min-w-0 flex-1">{children}</div>
</div>
);
}
@@ -230,11 +230,11 @@ function EmbeddingExampleCard({ providerId, customAlias }) {
{/* Endpoint */}
<Row label="Endpoint">
<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">
<input
value={endpoint}
onChange={(e) => useTunnel ? setTunnelEndpoint(e.target.value) : setLocalEndpoint(e.target.value)}
className="flex-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono"
className="w-full min-w-0 flex-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono"
placeholder="http://localhost:3000"
/>
{/* Tunnel toggle — only show if tunnel URL is available */}
@@ -298,12 +298,12 @@ function EmbeddingExampleCard({ providerId, customAlias }) {
{/* Curl + Run */}
<div className="mt-1">
<div className="flex items-center justify-between mb-1.5">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">Request</span>
<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">
<button
onClick={() => copyCurl(curlSnippet)}
className="flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">{copiedCurl ? "check" : "content_copy"}</span>
{copiedCurl ? "Copied" : "Copy"}
@@ -311,7 +311,7 @@ function EmbeddingExampleCard({ providerId, customAlias }) {
<button
onClick={handleRun}
disabled={running || !input.trim() || !modelFull}
className="flex items-center gap-1.5 px-3 py-1 rounded-lg bg-primary text-white text-xs font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
className="flex w-full sm:w-auto items-center justify-center gap-1.5 px-3 py-1 rounded-lg bg-primary text-white text-xs font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="material-symbols-outlined text-[14px]" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
play_arrow
@@ -320,7 +320,7 @@ function EmbeddingExampleCard({ providerId, customAlias }) {
</button>
</div>
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre">{curlSnippet}</pre>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all">{curlSnippet}</pre>
</div>
{/* Error */}
@@ -328,21 +328,21 @@ function EmbeddingExampleCard({ providerId, customAlias }) {
{/* Response — default example or real result */}
<div>
<div className="flex items-center justify-between mb-1.5">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">
Response {result && <span className="font-normal normal-case">&#9889; {result.latencyMs}ms</span>}
</span>
{result && (
<button
onClick={() => copyRes(resultJson)}
className="flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">{copiedRes ? "check" : "content_copy"}</span>
{copiedRes ? "Copied" : "Copy"}
</button>
)}
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre opacity-70">
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all opacity-70">
{formatResultJson(result?.data)}
</pre>
</div>
@@ -567,8 +567,8 @@ function TtsExampleCard({ providerId }) {
<div className="flex flex-col gap-2.5">
{/* Endpoint + API Key as read-only text */}
<Row label="Endpoint">
<div className="flex items-center gap-2">
<span className="flex-1 px-3 py-1.5 text-sm font-mono text-text-main bg-sidebar rounded-lg truncate">
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
<span className="w-full min-w-0 flex-1 px-3 py-1.5 text-sm font-mono text-text-main bg-sidebar rounded-lg truncate">
{endpoint}/v1/audio/speech
</span>
{tunnelEndpoint && (
@@ -611,10 +611,10 @@ function TtsExampleCard({ providerId }) {
{/* Language row + Browse button (edge-tts, local-device, elevenlabs) */}
{config.hasBrowseButton && (
<Row label="Language">
<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">
<button
onClick={openModal}
className="flex-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-background font-mono truncate text-left hover:border-primary/40 transition-colors"
className="w-full min-w-0 flex-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-background font-mono truncate text-left hover:border-primary/40 transition-colors"
>
{selectedLang
? <span className="text-text-main">{languages.find((l) => l.code === selectedLang)?.name || selectedLang}</span>
@@ -622,7 +622,7 @@ function TtsExampleCard({ providerId }) {
</button>
<button
onClick={openModal}
className="flex items-center gap-1 text-xs px-2.5 py-1.5 rounded-lg border border-border text-text-muted hover:text-primary hover:border-primary/40 transition-colors shrink-0"
className="flex w-full items-center justify-center gap-1 text-xs px-2.5 py-1.5 rounded-lg border border-border text-text-muted hover:text-primary hover:border-primary/40 transition-colors sm:w-auto sm:shrink-0"
>
<span className="material-symbols-outlined text-[14px]">language</span>
Select language
@@ -743,12 +743,12 @@ function TtsExampleCard({ providerId }) {
{/* Curl + Run */}
<div className="mt-1">
<div className="flex items-center justify-between mb-1.5">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">Request</span>
<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">
<button
onClick={() => copyCurl(curlSnippet)}
className="flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">{copiedCurl ? "check" : "content_copy"}</span>
{copiedCurl ? "Copied" : "Copy"}
@@ -756,7 +756,7 @@ function TtsExampleCard({ providerId }) {
<button
onClick={handleRun}
disabled={running || !input.trim() || !modelFull}
className="flex items-center gap-1.5 px-3 py-1 rounded-lg bg-primary text-white text-xs font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
className="flex w-full sm:w-auto items-center justify-center gap-1.5 px-3 py-1 rounded-lg bg-primary text-white text-xs font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="material-symbols-outlined text-[14px]" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
play_arrow
@@ -765,7 +765,7 @@ function TtsExampleCard({ providerId }) {
</button>
</div>
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre">{curlSnippet}</pre>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all">{curlSnippet}</pre>
</div>
{error && <p className="text-xs text-red-500 break-words">{error}</p>}
@@ -773,11 +773,11 @@ function TtsExampleCard({ providerId }) {
{/* Audio player */}
{audioUrl ? (
<div>
<div className="flex items-center justify-between mb-1.5">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">
Response {latency && <span className="font-normal normal-case">&#9889; {latency}ms</span>}
</span>
<a href={audioUrl} download="speech.mp3" className="flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors">
<a href={audioUrl} download="speech.mp3" className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors">
<span className="material-symbols-outlined text-[14px]">download</span>
Download
</a>
@@ -787,7 +787,7 @@ function TtsExampleCard({ providerId }) {
{/* JSON Response (if format is json) */}
{jsonResponse && (
<div className="mt-3">
<div className="flex items-center justify-between mb-1.5">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">JSON Response</span>
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all">
@@ -802,7 +802,7 @@ function TtsExampleCard({ providerId }) {
) : (
<div>
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">Response</span>
<pre className="mt-1.5 bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre opacity-50">{DEFAULT_TTS_RESPONSE_EXAMPLE}</pre>
<pre className="mt-1.5 bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all opacity-50">{DEFAULT_TTS_RESPONSE_EXAMPLE}</pre>
</div>
)}
</div>
@@ -811,7 +811,7 @@ function TtsExampleCard({ providerId }) {
{/* Country Picker Modal */}
{modalOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
className="fixed inset-0 z-50 flex items-end justify-center sm:items-center"
style={{ backgroundColor: "rgba(0,0,0,0.6)", backdropFilter: "blur(2px)" }}
onClick={() => setModalOpen(false)}
>
@@ -1077,8 +1077,8 @@ function GenericExampleCard({ providerId, kind }) {
{/* Endpoint */}
<Row label="Endpoint">
<div className="flex items-center gap-2">
<span className="flex-1 px-3 py-1.5 text-sm font-mono text-text-main bg-sidebar rounded-lg truncate">
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
<span className="w-full min-w-0 flex-1 px-3 py-1.5 text-sm font-mono text-text-main bg-sidebar rounded-lg truncate">
{endpoint}{apiPath}
</span>
{tunnelEndpoint && (
@@ -1232,12 +1232,12 @@ function GenericExampleCard({ providerId, kind }) {
{/* Curl + Run */}
<div className="mt-1">
<div className="flex items-center justify-between mb-1.5">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">Request</span>
<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">
<button
onClick={() => copyCurl(curlSnippet)}
className="flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">{copiedCurl ? "check" : "content_copy"}</span>
{copiedCurl ? "Copied" : "Copy"}
@@ -1245,7 +1245,7 @@ function GenericExampleCard({ providerId, kind }) {
<button
onClick={handleRun}
disabled={running || !input.trim() || !modelFull}
className="flex items-center gap-1.5 px-3 py-1 rounded-lg bg-primary text-white text-xs font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
className="flex w-full sm:w-auto items-center justify-center gap-1.5 px-3 py-1 rounded-lg bg-primary text-white text-xs font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="material-symbols-outlined text-[14px]" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
play_arrow
@@ -1254,12 +1254,12 @@ function GenericExampleCard({ providerId, kind }) {
</button>
</div>
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre">{curlSnippet}</pre>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all">{curlSnippet}</pre>
</div>
{/* Streaming progress */}
{(running || progress) && useStreaming && (
<div className="flex items-center gap-3 px-3 py-2 rounded-lg bg-sidebar border border-border">
<div className="flex flex-col gap-2 px-3 py-2 rounded-lg bg-sidebar border border-border sm:flex-row sm:items-center sm:gap-3">
<span className="material-symbols-outlined text-[16px] text-primary" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
{running ? "progress_activity" : "check_circle"}
</span>
@@ -1287,21 +1287,21 @@ function GenericExampleCard({ providerId, kind }) {
{/* Response */}
<div>
<div className="flex items-center justify-between mb-1.5">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between mb-1.5">
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">
Response {result && <span className="font-normal normal-case">&#9889; {result.latencyMs}ms</span>}
</span>
{result && (
<button
onClick={() => copyRes(resultJson)}
className="flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">{copiedRes ? "check" : "content_copy"}</span>
{copiedRes ? "Copied" : "Copy"}
</button>
)}
</div>
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre opacity-70">
<pre className="bg-sidebar rounded-lg px-3 py-2.5 text-xs font-mono text-text-main overflow-x-auto whitespace-pre-wrap break-all opacity-70">
{result ? resultJson : exConfig.defaultResponse}
</pre>
{kind === "image" && (binaryImageUrl || result?.data?.data?.[0]) && (
@@ -1310,7 +1310,7 @@ function GenericExampleCard({ providerId, kind }) {
<a
href={binaryImageUrl || (result?.data?.data?.[0]?.b64_json ? `data:image/png;base64,${result.data.data[0].b64_json}` : result?.data?.data?.[0]?.url || "")}
download="image.png"
className="flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">download</span>
Download
@@ -1396,7 +1396,7 @@ export default function MediaProviderDetailPage() {
</Link>
{/* Header */}
<div className="flex items-center gap-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
<div className="size-12 rounded-lg flex items-center justify-center shrink-0" style={{ backgroundColor: `${provider.color}15` }}>
<ProviderIcon
src={`/providers/${provider.id}.png`}
@@ -1408,7 +1408,7 @@ export default function MediaProviderDetailPage() {
/>
</div>
<div className="flex-1">
<div className="flex items-center gap-3 flex-wrap">
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
<h1 className="text-3xl font-semibold tracking-tight">{provider.name}</h1>
{!isCustom && provider.notice?.apiKeyUrl && (
<a
@@ -1432,7 +1432,7 @@ export default function MediaProviderDetailPage() {
</div>
</div>
{isCustom && (
<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">
<Button size="sm" variant="secondary" icon="edit" onClick={() => setShowEditModal(true)}>
Edit
</Button>
@@ -43,7 +43,7 @@ function MediaProviderCard({ provider, kind, connections, isCustom }) {
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 gap-3">
<div className="flex min-w-0 items-center gap-3">
<div
className="size-8 rounded-lg flex items-center justify-center shrink-0"
style={{ backgroundColor: `${provider.color?.length > 7 ? provider.color : (provider.color ?? "#888") + "15"}` }}
@@ -44,7 +44,7 @@ function ProviderPickerModal({ isOpen, onClose, onPick, kind, currentIds, connec
No connected providers available. Add a connection first in the {KIND_LABELS[kind]} section.
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 max-h-[400px] overflow-y-auto">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2 max-h-[400px] overflow-y-auto">
{providers.map((p) => {
const already = currentIds.includes(p.id);
return (
@@ -218,7 +218,7 @@ export default function ComboDetailPage() {
return (
<div className="flex flex-col gap-6">
{/* Header */}
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3 min-w-0">
<Link href="/dashboard/media-providers/web" className="text-text-muted hover:text-primary">
<span className="material-symbols-outlined">arrow_back</span>
@@ -256,7 +256,7 @@ export default function ComboDetailPage() {
{/* Providers Card */}
<Card>
<div className="flex items-center justify-between mb-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between mb-3">
<div>
<h2 className="text-lg font-semibold">Providers</h2>
<p className="text-xs text-text-muted">Tried in order (top-down) or rotated when round-robin is on.</p>
@@ -304,7 +304,7 @@ export default function ComboDetailPage() {
{/* Test Example Card */}
{combo.kind && (
<Card>
<div className="flex items-center justify-between mb-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between mb-3">
<h2 className="text-lg font-semibold">Test Example</h2>
<Button size="sm" icon="play_arrow" onClick={handleTest} disabled={testing || providers.length === 0}>
{testing ? "Running..." : "Run"}
@@ -39,7 +39,7 @@ function ProviderCard({ provider, kind, connections }) {
return (
<Link href={`/dashboard/media-providers/${kind}/${provider.id}`} className="group">
<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 gap-3">
<div className="flex min-w-0 items-center gap-3">
<div
className="size-8 rounded-lg flex items-center justify-center shrink-0"
style={{ backgroundColor: `${provider.color?.length > 7 ? provider.color : (provider.color ?? "#888") + "15"}` }}
@@ -72,11 +72,11 @@ function ComboList({ combos }) {
{combos.map((combo) => (
<Link key={combo.id} href={`/dashboard/media-providers/web/combo/${combo.id}`}>
<Card padding="xs" className="hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors cursor-pointer">
<div className="flex items-center gap-3">
<div className="flex min-w-0 items-center gap-3">
<span className="material-symbols-outlined text-primary text-[18px]">layers</span>
<code className="text-sm font-mono font-medium flex-1 truncate">{combo.name}</code>
{/* Provider icons preview */}
<div className="flex items-center gap-1 shrink-0">
<div className="flex flex-wrap items-center gap-1 sm:shrink-0">
{combo.models.slice(0, 6).map((pid, i) => {
const p = AI_PROVIDERS[pid];
return (
@@ -110,8 +110,8 @@ function Section({ title, icon, kind, providers, connections, combos, onCreateCo
return (
<div>
{/* Header — title left, Create Combo right */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between mb-3">
<div className="flex flex-wrap items-center gap-2">
<span className="material-symbols-outlined text-primary">{icon}</span>
<h2 className="text-base font-semibold">{title}</h2>
<span className="text-xs text-text-muted">({providers.length} providers · {combos.length} combos)</span>