mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
feat(cli-tools): add browser-local endpoint presets (#819)
Add reusable EndpointPresetControl for CLI tool Base URL/API key presets, stored in browser localStorage. Wire into Claude, Codex, OpenCode, Droid, OpenClaw, Hermes, and Copilot cards. Allow selecting preset API keys not in dashboard keys list. Thanks @dmdfami for the contribution! Co-authored-by: dmdfami <dmdfami@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal, Tooltip } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import EndpointPresetControl from "./EndpointPresetControl";
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
|
||||
@@ -135,6 +136,7 @@ export default function ClaudeToolCard({
|
||||
const url = customBaseUrl || baseUrl;
|
||||
return url.endsWith("/v1") ? url : `${url}/v1`;
|
||||
};
|
||||
const hasCustomSelectedApiKey = selectedApiKey && !apiKeys.some((key) => key.key === selectedApiKey);
|
||||
|
||||
const handleApplySettings = async () => {
|
||||
setApplying(true);
|
||||
@@ -301,6 +303,13 @@ export default function ClaudeToolCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EndpointPresetControl
|
||||
baseUrl={getDisplayUrl()}
|
||||
apiKey={selectedApiKey}
|
||||
onBaseUrlChange={setCustomBaseUrl}
|
||||
onApiKeyChange={setSelectedApiKey}
|
||||
/>
|
||||
|
||||
{/* Base URL */}
|
||||
<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>
|
||||
@@ -323,8 +332,9 @@ export default function ClaudeToolCard({
|
||||
<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 ? (
|
||||
{apiKeys.length > 0 || selectedApiKey ? (
|
||||
<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">
|
||||
{hasCustomSelectedApiKey && <option value={selectedApiKey}>{selectedApiKey}</option>}
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
@@ -393,4 +403,3 @@ export default function ClaudeToolCard({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import EndpointPresetControl from "./EndpointPresetControl";
|
||||
|
||||
export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled, initialStatus }) {
|
||||
const [codexStatus, setCodexStatus] = useState(initialStatus || null);
|
||||
@@ -76,6 +77,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
|
||||
};
|
||||
|
||||
const getDisplayUrl = () => customBaseUrl || `${baseUrl}/v1`;
|
||||
const hasCustomSelectedApiKey = selectedApiKey && !apiKeys.some((key) => key.key === selectedApiKey);
|
||||
|
||||
const checkCodexStatus = async () => {
|
||||
setCheckingCodex(true);
|
||||
@@ -279,6 +281,13 @@ model = "${effectiveSubagentModel}"
|
||||
) : null;
|
||||
})()}
|
||||
|
||||
<EndpointPresetControl
|
||||
baseUrl={getDisplayUrl()}
|
||||
apiKey={selectedApiKey}
|
||||
onBaseUrlChange={setCustomBaseUrl}
|
||||
onApiKeyChange={setSelectedApiKey}
|
||||
/>
|
||||
|
||||
{/* Base URL */}
|
||||
<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>
|
||||
@@ -301,8 +310,9 @@ model = "${effectiveSubagentModel}"
|
||||
<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 ? (
|
||||
{apiKeys.length > 0 || selectedApiKey ? (
|
||||
<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">
|
||||
{hasCustomSelectedApiKey && <option value={selectedApiKey}>{selectedApiKey}</option>}
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import EndpointPresetControl from "./EndpointPresetControl";
|
||||
|
||||
export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled, initialStatus }) {
|
||||
const [status, setStatus] = useState(initialStatus || null);
|
||||
@@ -11,6 +12,7 @@ export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, a
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
const [message, setMessage] = useState(null);
|
||||
const [selectedApiKey, setSelectedApiKey] = useState("");
|
||||
const [customBaseUrl, setCustomBaseUrl] = useState("");
|
||||
const [modelAliases, setModelAliases] = useState({});
|
||||
const [showManualConfigModal, setShowManualConfigModal] = useState(false);
|
||||
|
||||
@@ -66,7 +68,11 @@ export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, a
|
||||
};
|
||||
|
||||
const configStatus = getConfigStatus();
|
||||
const getEffectiveBaseUrl = () => baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
|
||||
const getEffectiveBaseUrl = () => {
|
||||
const url = customBaseUrl || baseUrl;
|
||||
return url.endsWith("/v1") ? url : `${url}/v1`;
|
||||
};
|
||||
const hasCustomSelectedApiKey = selectedApiKey && !apiKeys.some((key) => key.key === selectedApiKey);
|
||||
|
||||
const addModel = () => {
|
||||
const val = modelInput.trim();
|
||||
@@ -200,11 +206,30 @@ export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, a
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<EndpointPresetControl
|
||||
baseUrl={getEffectiveBaseUrl()}
|
||||
apiKey={selectedApiKey}
|
||||
onBaseUrlChange={setCustomBaseUrl}
|
||||
onApiKeyChange={setSelectedApiKey}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-text-muted">Base URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={getEffectiveBaseUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
className="px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-text-muted">API Key</label>
|
||||
{apiKeys.length > 0 ? (
|
||||
{apiKeys.length > 0 || selectedApiKey ? (
|
||||
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50">
|
||||
{hasCustomSelectedApiKey && <option value={selectedApiKey}>{selectedApiKey}</option>}
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import EndpointPresetControl from "./EndpointPresetControl";
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
|
||||
@@ -116,6 +117,7 @@ export default function DroidToolCard({
|
||||
const url = customBaseUrl || baseUrl;
|
||||
return url.endsWith("/v1") ? url : `${url}/v1`;
|
||||
};
|
||||
const hasCustomSelectedApiKey = selectedApiKey && !apiKeys.some((key) => key.key === selectedApiKey);
|
||||
|
||||
const addModel = () => {
|
||||
const val = modelInput.trim();
|
||||
@@ -296,6 +298,13 @@ export default function DroidToolCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EndpointPresetControl
|
||||
baseUrl={getDisplayUrl()}
|
||||
apiKey={selectedApiKey}
|
||||
onBaseUrlChange={setCustomBaseUrl}
|
||||
onApiKeyChange={setSelectedApiKey}
|
||||
/>
|
||||
|
||||
{/* Base URL */}
|
||||
<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>
|
||||
@@ -318,8 +327,9 @@ export default function DroidToolCard({
|
||||
<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 ? (
|
||||
{apiKeys.length > 0 || selectedApiKey ? (
|
||||
<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">
|
||||
{hasCustomSelectedApiKey && <option value={selectedApiKey}>{selectedApiKey}</option>}
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
@@ -415,4 +425,4 @@ export default function DroidToolCard({
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
const STORAGE_KEY = "9router.cliToolEndpointPresets";
|
||||
|
||||
function maskApiKey(apiKey) {
|
||||
if (!apiKey) return "No API key";
|
||||
if (apiKey.length <= 12) return `${apiKey.slice(0, 4)}...`;
|
||||
return `${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`;
|
||||
}
|
||||
|
||||
function normalizePresets(value) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter((preset) => preset?.name && preset?.baseUrl && preset?.apiKey);
|
||||
}
|
||||
|
||||
function readPresets() {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
return normalizePresets(JSON.parse(window.localStorage.getItem(STORAGE_KEY) || "[]"));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writePresets(presets) {
|
||||
if (typeof window === "undefined") return;
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(normalizePresets(presets)));
|
||||
}
|
||||
|
||||
export default function EndpointPresetControl({
|
||||
baseUrl,
|
||||
apiKey,
|
||||
onBaseUrlChange,
|
||||
onApiKeyChange,
|
||||
}) {
|
||||
const [presets, setPresets] = useState([]);
|
||||
const [selectedName, setSelectedName] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setPresets(readPresets());
|
||||
}, []);
|
||||
|
||||
const selectedPreset = useMemo(
|
||||
() => presets.find((preset) => preset.name === selectedName) || null,
|
||||
[presets, selectedName]
|
||||
);
|
||||
|
||||
const handleSelect = (name) => {
|
||||
setSelectedName(name);
|
||||
const preset = presets.find((item) => item.name === name);
|
||||
if (!preset) return;
|
||||
onBaseUrlChange(preset.baseUrl);
|
||||
onApiKeyChange(preset.apiKey);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const trimmedBaseUrl = (baseUrl || "").trim();
|
||||
const trimmedApiKey = (apiKey || "").trim();
|
||||
if (!trimmedBaseUrl || !trimmedApiKey) return;
|
||||
|
||||
let defaultName = selectedPreset?.name || trimmedBaseUrl;
|
||||
try {
|
||||
defaultName = selectedPreset?.name || new URL(trimmedBaseUrl).host;
|
||||
} catch {
|
||||
defaultName = selectedPreset?.name || trimmedBaseUrl;
|
||||
}
|
||||
const name = window.prompt("Preset name", defaultName);
|
||||
if (!name?.trim()) return;
|
||||
|
||||
const nextPreset = { name: name.trim(), baseUrl: trimmedBaseUrl, apiKey: trimmedApiKey };
|
||||
const nextPresets = [
|
||||
...presets.filter((preset) => preset.name !== nextPreset.name),
|
||||
nextPreset,
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
setPresets(nextPresets);
|
||||
setSelectedName(nextPreset.name);
|
||||
writePresets(nextPresets);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!selectedPreset) return;
|
||||
const nextPresets = presets.filter((preset) => preset.name !== selectedPreset.name);
|
||||
setPresets(nextPresets);
|
||||
setSelectedName("");
|
||||
writePresets(nextPresets);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Preset</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<select
|
||||
value={selectedName}
|
||||
onChange={(event) => handleSelect(event.target.value)}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
>
|
||||
<option value="">Manual / current endpoint</option>
|
||||
{presets.map((preset) => (
|
||||
<option key={preset.name} value={preset.name}>
|
||||
{preset.name} - {preset.baseUrl} ({maskApiKey(preset.apiKey)})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={!baseUrl || !apiKey}
|
||||
className="px-2 py-1.5 rounded border text-xs bg-surface border-border text-text-main hover:border-primary disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
|
||||
title="Save current Base URL and API key as a browser-local preset"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
{selectedPreset && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||
title="Delete selected preset"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">delete</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import EndpointPresetControl from "./EndpointPresetControl";
|
||||
|
||||
const ENDPOINT = "/api/cli-tools/hermes-settings";
|
||||
|
||||
@@ -104,6 +105,7 @@ export default function HermesToolCard({
|
||||
const url = customBaseUrl || getLocalBaseUrl();
|
||||
return url.endsWith("/v1") ? url : `${url}/v1`;
|
||||
};
|
||||
const hasCustomSelectedApiKey = selectedApiKey && !apiKeys.some((key) => key.key === selectedApiKey);
|
||||
|
||||
const handleApply = async () => {
|
||||
setApplying(true);
|
||||
@@ -237,6 +239,13 @@ export default function HermesToolCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EndpointPresetControl
|
||||
baseUrl={getEffectiveBaseUrl()}
|
||||
apiKey={selectedApiKey}
|
||||
onBaseUrlChange={setCustomBaseUrl}
|
||||
onApiKeyChange={setSelectedApiKey}
|
||||
/>
|
||||
|
||||
<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>
|
||||
@@ -257,8 +266,9 @@ export default function HermesToolCard({
|
||||
<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 ? (
|
||||
{apiKeys.length > 0 || selectedApiKey ? (
|
||||
<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">
|
||||
{hasCustomSelectedApiKey && <option value={selectedApiKey}>{selectedApiKey}</option>}
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import EndpointPresetControl from "./EndpointPresetControl";
|
||||
|
||||
export default function OpenClawToolCard({
|
||||
tool,
|
||||
@@ -122,6 +123,7 @@ export default function OpenClawToolCard({
|
||||
const url = customBaseUrl || getLocalBaseUrl();
|
||||
return url.endsWith("/v1") ? url : `${url}/v1`;
|
||||
};
|
||||
const hasCustomSelectedApiKey = selectedApiKey && !apiKeys.some((key) => key.key === selectedApiKey);
|
||||
|
||||
const handleApplySettings = async () => {
|
||||
setApplying(true);
|
||||
@@ -287,6 +289,13 @@ export default function OpenClawToolCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EndpointPresetControl
|
||||
baseUrl={getDisplayUrl()}
|
||||
apiKey={selectedApiKey}
|
||||
onBaseUrlChange={setCustomBaseUrl}
|
||||
onApiKeyChange={setSelectedApiKey}
|
||||
/>
|
||||
|
||||
{/* Base URL */}
|
||||
<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>
|
||||
@@ -309,8 +318,9 @@ export default function OpenClawToolCard({
|
||||
<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 ? (
|
||||
{apiKeys.length > 0 || selectedApiKey ? (
|
||||
<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">
|
||||
{hasCustomSelectedApiKey && <option value={selectedApiKey}>{selectedApiKey}</option>}
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import EndpointPresetControl from "./EndpointPresetControl";
|
||||
|
||||
export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders, cloudEnabled, initialStatus }) {
|
||||
const [status, setStatus] = useState(initialStatus || null);
|
||||
@@ -81,6 +82,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
||||
};
|
||||
|
||||
const getDisplayUrl = () => customBaseUrl || `${baseUrl}/v1`;
|
||||
const hasCustomSelectedApiKey = selectedApiKey && !apiKeys.some((key) => key.key === selectedApiKey);
|
||||
|
||||
const checkStatus = async () => {
|
||||
setChecking(true);
|
||||
@@ -266,6 +268,13 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EndpointPresetControl
|
||||
baseUrl={getDisplayUrl()}
|
||||
apiKey={selectedApiKey}
|
||||
onBaseUrlChange={setCustomBaseUrl}
|
||||
onApiKeyChange={setSelectedApiKey}
|
||||
/>
|
||||
|
||||
{/* Base URL */}
|
||||
<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>
|
||||
@@ -288,8 +297,9 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
||||
<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 ? (
|
||||
{apiKeys.length > 0 || selectedApiKey ? (
|
||||
<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">
|
||||
{hasCustomSelectedApiKey && <option value={selectedApiKey}>{selectedApiKey}</option>}
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
|
||||
@@ -10,4 +10,4 @@ export { default as CopilotToolCard } from "./CopilotToolCard";
|
||||
export { default as MitmServerCard } from "./MitmServerCard";
|
||||
export { default as MitmToolCard } from "./MitmToolCard";
|
||||
export { default as MitmLinkCard } from "./MitmLinkCard";
|
||||
|
||||
export { default as EndpointPresetControl } from "./EndpointPresetControl";
|
||||
|
||||
Reference in New Issue
Block a user