Improve mobile layouts and restore Cloudflare provider (#840)

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

View File

@@ -348,8 +348,19 @@ export const PROVIDER_MODELS = {
{ id: "DeepSeek-V3.2", name: "DeepSeek-V3.2" },
],
"cloudflare-ai": [
{ id: "@cf/meta/llama-3.2-1b-instruct", name: "Llama 3.2 1B Instruct" },
{ id: "@cf/meta/llama-3.2-3b-instruct", name: "Llama 3.2 3B Instruct" },
{ id: "@cf/meta/llama-3.1-8b-instruct-fp8-fast", name: "Llama 3.1 8B Instruct FP8 Fast" },
{ id: "@cf/meta/llama-3.1-8b-instruct-awq", name: "Llama 3.1 8B Instruct AWQ" },
{ id: "@cf/mistralai/mistral-small-3.1-24b-instruct", name: "Mistral Small 3.1 24B Instruct" },
{ id: "@cf/meta/llama-3.1-70b-instruct-fp8-fast", name: "Llama 3.1 70B Instruct FP8 Fast" },
{ id: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", name: "Llama 3.3 70B Instruct FP8 Fast" },
{ id: "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b", name: "DeepSeek R1 Distill Qwen 32B" },
{ id: "@cf/moonshotai/kimi-k2.5", name: "Kimi K2.5" },
{ id: "@cf/moonshotai/kimi-k2.6", name: "Kimi K2.6" },
{ id: "@cf/zai-org/glm-4.7-flash", name: "GLM 4.7 Flash" },
{ id: "@cf/qwen/qwq-32b", name: "QwQ 32B" },
{ id: "@cf/qwen/qwen2.5-coder-32b-instruct", name: "Qwen 2.5 Coder 32B Instruct" },
],
byteplus: [
{ id: "seed-2-0-pro-260328", name: "Seed 2.0 Pro" },

View File

@@ -65,6 +65,8 @@ const ALIAS_TO_PROVIDER_ID = {
"perplexity-web": "perplexity-web",
mimo: "xiaomi-mimo",
"xiaomi-mimo": "xiaomi-mimo",
cf: "cloudflare-ai",
"cloudflare-ai": "cloudflare-ai",
};
/**

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -290,7 +290,7 @@ export default function AntigravityToolCard({
</div>
{/* Start/Stop Button */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
{isRunning ? (
<button
onClick={handleStop}
@@ -322,14 +322,14 @@ export default function AntigravityToolCard({
{/* When running: API Key + Model Mappings */}
{isRunning && (
<>
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
{apiKeys.length > 0 ? (
<select
value={selectedApiKey}
onChange={(e) => setSelectedApiKey(e.target.value)}
className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
className="w-full min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
>
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
</select>
@@ -341,7 +341,7 @@ export default function AntigravityToolCard({
</div>
{tool.defaultModels.map((model) => (
<div key={model.alias} className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div key={model.alias} className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">{model.name}</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input
@@ -349,12 +349,12 @@ export default function AntigravityToolCard({
value={modelMappings[model.alias] || ""}
onChange={(e) => handleModelMappingChange(model.alias, e.target.value)}
placeholder="provider/model-id"
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
/>
<button
onClick={() => openModelSelector(model.alias)}
disabled={!hasActiveProviders}
className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
className={`w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
>
Select
</button>

View File

@@ -292,7 +292,7 @@ export default function ClaudeToolCard({
<div className="flex flex-col gap-2">
{/* Current Base URL */}
{claudeStatus?.settings?.env?.ANTHROPIC_BASE_URL && (
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Current</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
@@ -302,7 +302,7 @@ export default function ClaudeToolCard({
)}
{/* Base URL */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Base URL</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input
@@ -310,7 +310,7 @@ export default function ClaudeToolCard({
value={getDisplayUrl()}
onChange={(e) => setCustomBaseUrl(e.target.value)}
placeholder="https://.../v1"
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
/>
{customBaseUrl && customBaseUrl !== baseUrl && (
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
@@ -320,11 +320,11 @@ export default function ClaudeToolCard({
</div>
{/* API Key */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
{apiKeys.length > 0 ? (
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="w-full min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
</select>
) : (
@@ -336,17 +336,17 @@ export default function ClaudeToolCard({
{/* Model Mappings */}
{tool.defaultModels.map((model) => (
<div key={model.alias} className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div key={model.alias} className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">{model.name}</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input type="text" value={modelMappings[model.alias] || ""} onChange={(e) => onModelMappingChange(model.alias, e.target.value)} placeholder="provider/model-id" className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
<button onClick={() => openModelSelector(model.alias)} disabled={!hasActiveProviders} className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select Model</button>
<input type="text" value={modelMappings[model.alias] || ""} onChange={(e) => onModelMappingChange(model.alias, e.target.value)} placeholder="provider/model-id" className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
<button onClick={() => openModelSelector(model.alias)} disabled={!hasActiveProviders} className={`w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select Model</button>
{modelMappings[model.alias] && <button onClick={() => onModelMappingChange(model.alias, "")} className="p-1 text-text-muted hover:text-red-500 rounded transition-colors" title="Clear"><span className="material-symbols-outlined text-[14px]">close</span></button>}
</div>
))}
{/* CC Filter Naming */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Filter naming</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<label className="flex items-center gap-1.5 cursor-pointer select-none">

View File

@@ -269,7 +269,7 @@ model = "${effectiveSubagentModel}"
const parsed = codexStatus.config.match(/base_url\s*=\s*"([^"]+)"/);
const currentBaseUrl = parsed ? parsed[1] : null;
return currentBaseUrl ? (
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Current</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
@@ -280,7 +280,7 @@ model = "${effectiveSubagentModel}"
})()}
{/* Base URL */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Base URL</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input
@@ -288,7 +288,7 @@ model = "${effectiveSubagentModel}"
value={getDisplayUrl()}
onChange={(e) => setCustomBaseUrl(e.target.value)}
placeholder="https://.../v1"
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
/>
{customBaseUrl && customBaseUrl !== `${baseUrl}/v1` && (
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
@@ -298,11 +298,11 @@ model = "${effectiveSubagentModel}"
</div>
{/* API Key */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
{apiKeys.length > 0 ? (
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="w-full min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
</select>
) : (
@@ -313,16 +313,16 @@ model = "${effectiveSubagentModel}"
</div>
{/* Model */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Model</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
<button onClick={() => setModalOpen(true)} disabled={!activeProviders?.length} className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select Model</button>
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
<button onClick={() => setModalOpen(true)} disabled={!activeProviders?.length} className={`w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select Model</button>
{selectedModel && <button onClick={() => setSelectedModel("")} className="p-1 text-text-muted hover:text-red-500 rounded transition-colors" title="Clear"><span className="material-symbols-outlined text-[14px]">close</span></button>}
</div>
{/* Subagent Model */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Subagent Model</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input
@@ -330,12 +330,12 @@ model = "${effectiveSubagentModel}"
value={subagentModel}
onChange={(e) => setSubagentModel(e.target.value)}
placeholder={selectedModel || "provider/model-id (defaults to main model)"}
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
/>
<button
onClick={() => setSubagentModalOpen(true)}
disabled={!activeProviders?.length}
className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
className={`w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
>
Select Model
</button>

View File

@@ -48,13 +48,13 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
const renderApiKeySelector = () => {
return (
<div className="mt-2 flex items-center gap-2">
<div className="mt-2 flex flex-col sm:flex-row sm:items-center gap-2">
{apiKeys && apiKeys.length > 0 ? (
<>
<select
value={selectedApiKey}
onChange={(e) => setSelectedApiKey(e.target.value)}
className="flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
className="w-full sm:w-auto flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
>
{apiKeys.map((key) => (
<option key={key.id} value={key.key}>{key.key}</option>
@@ -80,13 +80,13 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
const renderModelSelector = () => {
return (
<div className="mt-2 flex items-center gap-2">
<div className="mt-2 flex flex-col sm:flex-row sm:items-center gap-2">
<input
type="text"
value={modelValue}
onChange={(e) => setModelValue(e.target.value)}
placeholder="provider/model-id"
className="flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
className="w-full sm:w-auto flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
/>
<button
onClick={() => setShowModelModal(true)}
@@ -188,8 +188,8 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
{item.type === "apiKeySelector" && renderApiKeySelector()}
{item.type === "modelSelector" && renderModelSelector()}
{item.value && (
<div className="mt-2 flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm font-mono border border-border truncate">
<div className="mt-2 flex flex-col sm:flex-row sm:items-center gap-2">
<code className="w-full sm:w-auto flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm font-mono border border-border truncate">
{replaceVars(item.value)}
</code>
{item.copyable && (
@@ -262,7 +262,7 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
};
return (
<Card padding="xs" className="overflow-hidden">
<Card padding="xs" className="overflow-hidden overflow-x-hidden">
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
<div className="flex items-center gap-3">
<div className="size-8 rounded-lg flex items-center justify-center shrink-0">

View File

@@ -287,7 +287,7 @@ export default function DroidToolCard({
<div className="flex flex-col gap-2">
{/* Current Base URL */}
{droidStatus?.settings?.customModels?.find(m => m.id?.startsWith("custom:9Router"))?.baseUrl && (
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Current</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
@@ -297,7 +297,7 @@ export default function DroidToolCard({
)}
{/* Base URL */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Base URL</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input
@@ -305,7 +305,7 @@ export default function DroidToolCard({
value={getDisplayUrl()}
onChange={(e) => setCustomBaseUrl(e.target.value)}
placeholder="https://.../v1"
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
/>
{customBaseUrl && customBaseUrl !== baseUrl && (
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
@@ -315,11 +315,11 @@ export default function DroidToolCard({
</div>
{/* API Key */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
{apiKeys.length > 0 ? (
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="w-full min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
</select>
) : (
@@ -330,7 +330,7 @@ export default function DroidToolCard({
</div>
{/* Models */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">
Models {modelList.length > 0 && <span className="text-primary">({modelList.length})</span>}
</span>
@@ -357,7 +357,7 @@ export default function DroidToolCard({
onChange={(e) => setModelInput(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addModel(); } }}
placeholder="provider/model-id"
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
/>
<button
onClick={() => setModalOpen(true)}

View File

@@ -214,8 +214,8 @@ export default function HermesToolCard({
<p className="text-sm text-text-muted">Install: curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash</p>
</div>
</div>
<div className="flex items-center gap-2 pl-9">
<Button variant="secondary" size="sm" onClick={() => setShowManualConfigModal(true)} className="!bg-yellow-500/20 !border-yellow-500/40 !text-yellow-700 dark:!text-yellow-300 hover:!bg-yellow-500/30">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 pl-0 sm:pl-9">
<Button variant="secondary" size="sm" onClick={() => setShowManualConfigModal(true)} className="w-full sm:w-auto !bg-yellow-500/20 !border-yellow-500/40 !text-yellow-700 dark:!text-yellow-300 hover:!bg-yellow-500/30">
<span className="material-symbols-outlined text-[18px] mr-1">content_copy</span>
Manual Config
</Button>
@@ -228,7 +228,7 @@ export default function HermesToolCard({
<>
<div className="flex flex-col gap-2">
{hermesStatus?.settings?.model?.base_url && (
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Current</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
@@ -237,7 +237,7 @@ export default function HermesToolCard({
</div>
)}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Base URL</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input
@@ -245,7 +245,7 @@ export default function HermesToolCard({
value={getEffectiveBaseUrl()}
onChange={(e) => setCustomBaseUrl(e.target.value)}
placeholder="https://.../v1"
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
/>
{customBaseUrl && customBaseUrl !== baseUrl && (
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
@@ -254,11 +254,11 @@ export default function HermesToolCard({
)}
</div>
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
{apiKeys.length > 0 ? (
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="w-full min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
</select>
) : (
@@ -268,11 +268,11 @@ export default function HermesToolCard({
)}
</div>
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Default Model</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
<button onClick={() => setModalOpen(true)} disabled={!hasActiveProviders} className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
<button onClick={() => setModalOpen(true)} disabled={!hasActiveProviders} className={`w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
{selectedModel && <button onClick={() => setSelectedModel("")} className="p-1 text-text-muted hover:text-red-500 rounded transition-colors" title="Clear"><span className="material-symbols-outlined text-[14px]">close</span></button>}
</div>
</div>
@@ -284,14 +284,14 @@ export default function HermesToolCard({
</div>
)}
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
<Button variant="primary" size="sm" onClick={handleApply} disabled={!selectedModel} loading={applying}>
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
<Button variant="primary" size="sm" onClick={handleApply} disabled={!selectedModel} loading={applying} className="w-full sm:w-auto">
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
</Button>
<Button variant="outline" size="sm" onClick={handleReset} disabled={!hermesStatus?.has9Router} loading={restoring}>
<Button variant="outline" size="sm" onClick={handleReset} disabled={!hermesStatus?.has9Router} loading={restoring} className="w-full sm:w-auto">
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>Reset
</Button>
<Button variant="ghost" size="sm" onClick={() => setShowManualConfigModal(true)}>
<Button variant="ghost" size="sm" onClick={() => setShowManualConfigModal(true)} className="w-full sm:w-auto">
<span className="material-symbols-outlined text-[14px] mr-1">content_copy</span>Manual Config
</Button>
</div>

View File

@@ -191,7 +191,7 @@ export default function MitmToolCard({
{tool.defaultModels?.length > 0 && (
<div className="flex flex-col gap-2">
{tool.defaultModels.map((model) => (
<div key={model.alias} className="grid gap-1.5 sm:grid-cols-[9rem_auto_1fr_auto_auto] sm:items-center sm:gap-2">
<div key={model.alias} className="grid grid-cols-1 gap-1.5 sm:grid-cols-[9rem_auto_1fr_auto_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right">{model.name}</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input

View File

@@ -278,7 +278,7 @@ export default function OpenClawToolCard({
<div className="flex flex-col gap-2">
{/* Current Base URL */}
{openclawStatus?.settings?.models?.providers?.["9router"]?.baseUrl && (
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Current</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
@@ -288,7 +288,7 @@ export default function OpenClawToolCard({
)}
{/* Base URL */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Base URL</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input
@@ -296,7 +296,7 @@ export default function OpenClawToolCard({
value={getDisplayUrl()}
onChange={(e) => setCustomBaseUrl(e.target.value)}
placeholder="https://.../v1"
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
/>
{customBaseUrl && customBaseUrl !== baseUrl && (
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
@@ -306,11 +306,11 @@ export default function OpenClawToolCard({
</div>
{/* API Key */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
{apiKeys.length > 0 ? (
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="w-full min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
</select>
) : (
@@ -321,11 +321,11 @@ export default function OpenClawToolCard({
</div>
{/* Default Model */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Default Model</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
<button onClick={() => { setAgentModalFor(null); setModalOpen(true); }} disabled={!hasActiveProviders} className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
<button onClick={() => { setAgentModalFor(null); setModalOpen(true); }} disabled={!hasActiveProviders} className={`w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
{selectedModel && <button onClick={() => setSelectedModel("")} className="p-1 text-text-muted hover:text-red-500 rounded transition-colors" title="Clear"><span className="material-symbols-outlined text-[14px]">close</span></button>}
</div>
@@ -339,9 +339,9 @@ export default function OpenClawToolCard({
value={agentModels[agent.id] || ""}
onChange={(e) => setAgentModels(prev => ({ ...prev, [agent.id]: e.target.value }))}
placeholder={`default (${selectedModel || "provider/model-id"})`}
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
/>
<button onClick={() => { setAgentModalFor(agent.id); setModalOpen(true); }} disabled={!hasActiveProviders} className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
<button onClick={() => { setAgentModalFor(agent.id); setModalOpen(true); }} disabled={!hasActiveProviders} className={`w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
{agentModels[agent.id] && <button onClick={() => setAgentModels(prev => ({ ...prev, [agent.id]: "" }))} className="p-1 text-text-muted hover:text-red-500 rounded transition-colors" title="Clear"><span className="material-symbols-outlined text-[14px]">close</span></button>}
</div>
))}

View File

@@ -257,7 +257,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
<div className="flex flex-col gap-2">
{/* Current base URL */}
{status?.config?.provider?.["9router"]?.options?.baseURL && (
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Current</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
@@ -267,7 +267,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
)}
{/* Base URL */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Base URL</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input
@@ -275,7 +275,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
value={getDisplayUrl()}
onChange={(e) => setCustomBaseUrl(e.target.value)}
placeholder="https://.../v1"
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
/>
{customBaseUrl && customBaseUrl !== `${baseUrl}/v1` && (
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
@@ -285,11 +285,11 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
</div>
{/* API Key */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
{apiKeys.length > 0 ? (
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="w-full min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
</select>
) : (
@@ -300,7 +300,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
</div>
{/* Models */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr] sm:items-start sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr] sm:items-start sm:gap-2">
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right pt-1">Models</span>
<span className="material-symbols-outlined text-text-muted text-[14px] mt-1.5">arrow_forward</span>
<div className="flex-1 flex flex-col gap-2">
@@ -364,7 +364,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
))
)}
</div>
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<button onClick={() => setModalOpen(true)} disabled={!activeProviders?.length} className={`px-2 py-1 rounded border text-xs transition-colors ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Add Model</button>
<span className="text-xs text-text-muted">
{selectedModels.length > 0 && activeModel ? (
@@ -380,7 +380,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
</div>
{/* Subagent Model */}
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Subagent Model</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<input
@@ -388,12 +388,12 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
value={subagentModel}
onChange={(e) => setSubagentModel(e.target.value)}
placeholder={selectedModel || "provider/model-id (defaults to main model)"}
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
/>
<button
onClick={() => setSubagentModalOpen(true)}
disabled={!activeProviders?.length}
className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
className={`w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
>
Select Model
</button>

View File

@@ -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>

View File

@@ -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"}` }}

View File

@@ -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"}

View File

@@ -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>

View File

@@ -104,8 +104,8 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov
};
return (
<div className={`group flex items-center justify-between p2 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 flex-col gap-3 p-2 rounded-lg sm:flex-row sm:items-center sm:justify-between hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors ${connection.isActive === false ? "opacity-60" : ""}`}>
<div className="flex w-full min-w-0 flex-1 items-start gap-3 sm:items-center">
<div className="flex flex-col">
<button onClick={onMoveUp} disabled={isFirst} className={`p-0.5 rounded ${isFirst ? "text-text-muted/30 cursor-not-allowed" : "hover:bg-sidebar text-text-muted hover:text-primary"}`}>
<span className="material-symbols-outlined text-sm">keyboard_arrow_up</span>
@@ -117,7 +117,7 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov
<span className="material-symbols-outlined 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 flex-wrap">
<div className="flex flex-wrap items-center gap-2 mt-1">
<Badge variant={getStatusVariant()} size="sm" dot>
{connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")}
</Badge>
@@ -129,7 +129,7 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov
<span className="text-xs text-text-muted">#{connection.priority}</span>
</div>
{hasAnyProxy && (
<div className="mt-1 flex items-center gap-2 flex-wrap">
<div className="mt-1 flex flex-wrap items-center gap-2">
<span className="text-[11px] text-text-muted truncate 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">{maskedProxyUrl}</code>}
{noProxyText && <span className="text-[11px] text-text-muted truncate max-w-[320px]" title={noProxyText}>no_proxy: {noProxyText}</span>}
@@ -137,8 +137,8 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov
)}
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex gap-1">
<div className="flex w-full flex-wrap items-center justify-between gap-2 sm:w-auto sm:justify-end">
<div className="flex flex-wrap gap-1">
{(proxyPools || []).length > 0 && (
<div className="relative" ref={proxyDropdownRef}>
<button
@@ -398,9 +398,9 @@ export default function ConnectionsCard({ providerId, isOAuth }) {
return (
<>
<Card>
<div className="flex items-center justify-between mb-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between mb-4">
<h2 className="text-lg font-semibold">Connections</h2>
<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"}
@@ -412,12 +412,12 @@ export default function ConnectionsCard({ providerId, isOAuth }) {
}}
/>
{providerStrategy === "round-robin" && (
<div className="flex items-center gap-1.5">
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-xs text-text-muted">Sticky:</span>
<input
type="number" min={1} value={providerStickyLimit}
onChange={(e) => { setProviderStickyLimit(e.target.value); saveStrategy("round-robin", e.target.value); }}
className="w-14 px-2 py-1 text-xs border border-border rounded-md bg-background focus:outline-none focus:border-primary"
className="w-16 px-2 py-1 text-xs border border-border rounded-md bg-background focus:outline-none focus:border-primary"
/>
</div>
)}
@@ -425,7 +425,7 @@ export default function ConnectionsCard({ providerId, isOAuth }) {
</div>
{connections.length === 0 ? (
<div className="flex items-center justify-between">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm text-text-muted">No connections yet</p>
<Button size="sm" icon="add" onClick={() => setShowAddModal(true)}>Add Connection</Button>
</div>
@@ -449,7 +449,7 @@ export default function ConnectionsCard({ providerId, isOAuth }) {
/>
))}
</div>
<div className="mt-4">
<div className="mt-4 flex justify-stretch sm:justify-start">
<Button size="sm" icon="add" onClick={() => setShowAddModal(true)}>Add</Button>
</div>
</>

View File

@@ -22,7 +22,6 @@ const navItems = [
// { href: "/dashboard/basic-chat", label: "Basic Chat", icon: "chat" }, // Hidden
{ href: "/dashboard/combos", label: "Combos", icon: "layers" },
{ href: "/dashboard/usage", label: "Usage", icon: "bar_chart" },
{ href: "/dashboard/auth-files", label: "Auth Files", icon: "vpn_key" },
{ href: "/dashboard/quota", label: "Quota Tracker", icon: "data_usage" },
{ href: "/dashboard/mitm", label: "MITM", icon: "security" },
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },

View File

@@ -19,6 +19,7 @@ export const FREE_TIER_PROVIDERS = {
ollama: { id: "ollama", alias: "ollama", name: "Ollama Cloud", icon: "cloud", color: "#ffffffff", textIcon: "OL", website: "https://ollama.com", notice: { text: "Free tier: light usage, 1 cloud model at a time (limits reset every 5h & 7d). Pro $20/mo · Max $100/mo.", apiKeyUrl: "https://ollama.com/settings/keys" } },
vertex: { id: "vertex", alias: "vx", name: "Vertex AI", icon: "cloud", color: "#4285F4", textIcon: "VX", website: "https://cloud.google.com/vertex-ai", notice: { text: "New Google Cloud accounts get $300 free credits. Requires GCP project + Service Account with Vertex AI API enabled.", apiKeyUrl: "https://console.cloud.google.com/iam-admin/serviceaccounts" } },
gemini: { id: "gemini", alias: "gemini", name: "Gemini", icon: "diamond", color: "#4285F4", textIcon: "GE", website: "https://ai.google.dev", notice: { apiKeyUrl: "https://aistudio.google.com/app/apikey" }, serviceKinds: ["llm", "embedding", "image", "imageToText", "webSearch"], searchViaChat: { defaultModel: "gemini-2.5-flash", pricingUrl: "https://ai.google.dev/pricing", freeTier: "Free tier: 15 RPM, 1M tokens/day on gemini-2.5-flash via AI Studio." }, embeddingConfig: { baseUrl: "https://generativelanguage.googleapis.com/v1beta/models", authType: "apikey", authHeader: "key", models: [{ id: "text-embedding-004", name: "Text Embedding 004", dimensions: 768 }, { id: "embedding-001", name: "Embedding 001", dimensions: 768 }] } },
"cloudflare-ai": { id: "cloudflare-ai", alias: "cf", name: "Cloudflare", icon: "cloud", color: "#F38020", textIcon: "CF", website: "https://developers.cloudflare.com/workers-ai/", notice: { text: "Workers AI free tier. Requires a Cloudflare API token and Account ID.", apiKeyUrl: "https://dash.cloudflare.com/profile/api-tokens" }, serviceKinds: ["llm"], hasProviderSpecificData: true },
byteplus: { id: "byteplus", alias: "bpm", name: "BytePlus ModelArk", icon: "cloud", color: "#2563EB", textIcon: "BP", website: "https://console.byteplus.com/ark", notice: { text: "Free credits for new accounts. Access to Seed 2.0, Kimi K2 Thinking, GLM 4.7, GPT-OSS-120B models.", apiKeyUrl: "https://console.byteplus.com/ark/region:ark+ap-southeast-1/apiKey" }, serviceKinds: ["llm"] },
};