mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
Improve mobile layouts and restore Cloudflare provider (#840)
Co-authored-by: Delynn Assistant <zhen@dkzhen.org>
This commit is contained in:
@@ -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" },
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
BIN
public/providers/cloudflare-ai.png
Normal file
BIN
public/providers/cloudflare-ai.png
Normal file
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 |
@@ -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>
|
||||
|
||||
@@ -95,7 +95,7 @@ export default function ClaudeToolCard({
|
||||
if (claudeStatus?.installed && !hasInitializedModels.current) {
|
||||
hasInitializedModels.current = true;
|
||||
const env = claudeStatus.settings?.env || {};
|
||||
|
||||
|
||||
tool.defaultModels.forEach((model) => {
|
||||
if (model.envKey) {
|
||||
const value = env[model.envKey] || model.defaultValue || "";
|
||||
@@ -141,16 +141,16 @@ export default function ClaudeToolCard({
|
||||
setMessage(null);
|
||||
try {
|
||||
const env = { ANTHROPIC_BASE_URL: getEffectiveBaseUrl() };
|
||||
|
||||
|
||||
// Get key from dropdown, fallback to first key or sk_9router for localhost
|
||||
const keyToUse = selectedApiKey?.trim()
|
||||
const keyToUse = selectedApiKey?.trim()
|
||||
|| (apiKeys?.length > 0 ? apiKeys[0].key : null)
|
||||
|| (!cloudEnabled ? "sk_9router" : null);
|
||||
|
||||
|
||||
if (keyToUse) {
|
||||
env.ANTHROPIC_AUTH_TOKEN = keyToUse;
|
||||
}
|
||||
|
||||
|
||||
tool.defaultModels.forEach((model) => {
|
||||
const targetModel = modelMappings[model.alias];
|
||||
if (targetModel && model.envKey) env[model.envKey] = targetModel;
|
||||
@@ -205,15 +205,15 @@ export default function ClaudeToolCard({
|
||||
|
||||
// Generate settings.json content for manual copy
|
||||
const getManualConfigs = () => {
|
||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||
? selectedApiKey
|
||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||
? selectedApiKey
|
||||
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
||||
const env = { ANTHROPIC_BASE_URL: getEffectiveBaseUrl(), ANTHROPIC_AUTH_TOKEN: keyToUse };
|
||||
tool.defaultModels.forEach((model) => {
|
||||
const targetModel = modelMappings[model.alias];
|
||||
if (targetModel && model.envKey) env[model.envKey] = targetModel;
|
||||
});
|
||||
|
||||
|
||||
return [
|
||||
{
|
||||
filename: "~/.claude/settings.json",
|
||||
@@ -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,15 +302,15 @@ 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
|
||||
type="text"
|
||||
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"
|
||||
<input
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
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">
|
||||
@@ -383,7 +383,7 @@ export default function ClaudeToolCard({
|
||||
)}
|
||||
|
||||
<ModelSelectModal isOpen={modalOpen} onClose={() => setModalOpen(false)} onSelect={handleModelSelect} selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null} activeProviders={activeProviders} modelAliases={modelAliases} title={`Select model for ${currentEditingAlias}`} />
|
||||
|
||||
|
||||
<ManualConfigModal
|
||||
isOpen={showManualConfigModal}
|
||||
onClose={() => setShowManualConfigModal(false)}
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
|
||||
if (codexStatus?.config) {
|
||||
const modelMatch = codexStatus.config.match(/^model\s*=\s*"([^"]+)"/m);
|
||||
if (modelMatch) setSelectedModel(modelMatch[1]);
|
||||
|
||||
|
||||
// Parse subagent settings
|
||||
const subagentModelMatch = codexStatus.config.match(/\[agents\.subagent\]\s*\n\s*model\s*=\s*"([^"]+)"/m);
|
||||
if (subagentModelMatch) setSubagentModel(subagentModelMatch[1]);
|
||||
@@ -74,7 +74,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
|
||||
// Ensure URL ends with /v1
|
||||
return url.endsWith("/v1") ? url : `${url}/v1`;
|
||||
};
|
||||
|
||||
|
||||
const getDisplayUrl = () => customBaseUrl || `${baseUrl}/v1`;
|
||||
|
||||
const checkCodexStatus = async () => {
|
||||
@@ -95,16 +95,16 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
|
||||
setMessage(null);
|
||||
try {
|
||||
// Use sk_9router for localhost if no key, otherwise use selected key
|
||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||
? selectedApiKey
|
||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||
? selectedApiKey
|
||||
: (!cloudEnabled ? "sk_9router" : selectedApiKey);
|
||||
|
||||
|
||||
const res = await fetch("/api/cli-tools/codex-settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
baseUrl: getEffectiveBaseUrl(),
|
||||
apiKey: keyToUse,
|
||||
body: JSON.stringify({
|
||||
baseUrl: getEffectiveBaseUrl(),
|
||||
apiKey: keyToUse,
|
||||
model: selectedModel,
|
||||
subagentModel: subagentModel || selectedModel
|
||||
}),
|
||||
@@ -154,12 +154,12 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
|
||||
};
|
||||
|
||||
const getManualConfigs = () => {
|
||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||
? selectedApiKey
|
||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||
? selectedApiKey
|
||||
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
||||
|
||||
|
||||
const effectiveSubagentModel = subagentModel || selectedModel;
|
||||
|
||||
|
||||
const configContent = `# 9Router Configuration for Codex CLI
|
||||
model = "${selectedModel}"
|
||||
model_provider = "9router"
|
||||
@@ -251,7 +251,7 @@ model = "${effectiveSubagentModel}"
|
||||
<p className="text-text-muted">After installation, run <code className="px-1 bg-black/5 dark:bg-white/5 rounded">codex</code> to verify.</p>
|
||||
<div className="pt-2 border-t border-border">
|
||||
<p className="text-text-muted text-xs">
|
||||
Codex uses <code className="px-1 bg-black/5 dark:bg-white/5 rounded">~/.codex/auth.json</code> with <code className="px-1 bg-black/5 dark:bg-white/5 rounded">OPENAI_API_KEY</code>.
|
||||
Codex uses <code className="px-1 bg-black/5 dark:bg-white/5 rounded">~/.codex/auth.json</code> with <code className="px-1 bg-black/5 dark:bg-white/5 rounded">OPENAI_API_KEY</code>.
|
||||
Click "Apply" to auto-configure.
|
||||
</p>
|
||||
</div>
|
||||
@@ -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,15 +280,15 @@ 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
|
||||
type="text"
|
||||
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"
|
||||
<input
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
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,36 +313,36 @@ 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
|
||||
type="text"
|
||||
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"
|
||||
<input
|
||||
type="text"
|
||||
value={subagentModel}
|
||||
onChange={(e) => setSubagentModel(e.target.value)}
|
||||
placeholder={selectedModel || "provider/model-id (defaults to main model)"}
|
||||
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"}`}
|
||||
<button
|
||||
onClick={() => setSubagentModalOpen(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>
|
||||
{subagentModel && (
|
||||
<button
|
||||
onClick={() => setSubagentModel("")}
|
||||
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||
<button
|
||||
onClick={() => setSubagentModel("")}
|
||||
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||
title="Clear (will use main model)"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">close</span>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -127,15 +127,15 @@ export default function OpenClawToolCard({
|
||||
setApplying(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
const keyToUse = selectedApiKey?.trim()
|
||||
const keyToUse = selectedApiKey?.trim()
|
||||
|| (apiKeys?.length > 0 ? apiKeys[0].key : null)
|
||||
|| (!cloudEnabled ? "sk_9router" : null);
|
||||
|
||||
const res = await fetch("/api/cli-tools/openclaw-settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
baseUrl: getEffectiveBaseUrl(),
|
||||
body: JSON.stringify({
|
||||
baseUrl: getEffectiveBaseUrl(),
|
||||
apiKey: keyToUse,
|
||||
model: selectedModel,
|
||||
agentModels,
|
||||
@@ -187,8 +187,8 @@ export default function OpenClawToolCard({
|
||||
};
|
||||
|
||||
const getManualConfigs = () => {
|
||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||
? selectedApiKey
|
||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||
? selectedApiKey
|
||||
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
||||
|
||||
const settingsContent = {
|
||||
@@ -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,15 +288,15 @@ 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
|
||||
type="text"
|
||||
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"
|
||||
<input
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
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>
|
||||
))}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
||||
if (status?.opencode?.activeModel) {
|
||||
setActiveModel(status.opencode.activeModel);
|
||||
}
|
||||
|
||||
|
||||
// Parse subagent settings from agent.explorer if exists
|
||||
if (status?.config?.agent?.explorer?.model?.startsWith("9router/")) {
|
||||
setSubagentModel(status.config.agent.explorer.model.replace("9router/", ""));
|
||||
@@ -106,9 +106,9 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
||||
const res = await fetch("/api/cli-tools/opencode-settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
baseUrl: getEffectiveBaseUrl(),
|
||||
apiKey: keyToUse,
|
||||
body: JSON.stringify({
|
||||
baseUrl: getEffectiveBaseUrl(),
|
||||
apiKey: keyToUse,
|
||||
models: selectedModels,
|
||||
activeModel: activeModel === "" ? "" : (activeModel || selectedModels[0]),
|
||||
subagentModel: subagentModel
|
||||
@@ -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,27 +380,27 @@ 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
|
||||
type="text"
|
||||
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"
|
||||
<input
|
||||
type="text"
|
||||
value={subagentModel}
|
||||
onChange={(e) => setSubagentModel(e.target.value)}
|
||||
placeholder={selectedModel || "provider/model-id (defaults to main model)"}
|
||||
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"}`}
|
||||
<button
|
||||
onClick={() => setSubagentModalOpen(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>
|
||||
{subagentModel && (
|
||||
<button
|
||||
onClick={() => setSubagentModel("")}
|
||||
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||
<button
|
||||
onClick={() => setSubagentModel("")}
|
||||
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||
title="Clear (will use main model)"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">close</span>
|
||||
@@ -435,7 +435,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
||||
<ModelSelectModal
|
||||
isOpen={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
onSelect={(model) => {
|
||||
onSelect={(model) => {
|
||||
if (!selectedModels.includes(model.value)) {
|
||||
setSelectedModels([...selectedModels, model.value]);
|
||||
if (!activeModel) setActiveModel(model.value);
|
||||
|
||||
@@ -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">⚡ {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">⚡ {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">⚡ {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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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"] },
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user