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" },
|
{ id: "DeepSeek-V3.2", name: "DeepSeek-V3.2" },
|
||||||
],
|
],
|
||||||
"cloudflare-ai": [
|
"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/moonshotai/kimi-k2.6", name: "Kimi K2.6" },
|
||||||
{ id: "@cf/zai-org/glm-4.7-flash", name: "GLM 4.7 Flash" },
|
{ 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: [
|
byteplus: [
|
||||||
{ id: "seed-2-0-pro-260328", name: "Seed 2.0 Pro" },
|
{ id: "seed-2-0-pro-260328", name: "Seed 2.0 Pro" },
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ const ALIAS_TO_PROVIDER_ID = {
|
|||||||
"perplexity-web": "perplexity-web",
|
"perplexity-web": "perplexity-web",
|
||||||
mimo: "xiaomi-mimo",
|
mimo: "xiaomi-mimo",
|
||||||
"xiaomi-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>
|
</div>
|
||||||
|
|
||||||
{/* Start/Stop Button */}
|
{/* 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 ? (
|
{isRunning ? (
|
||||||
<button
|
<button
|
||||||
onClick={handleStop}
|
onClick={handleStop}
|
||||||
@@ -322,14 +322,14 @@ export default function AntigravityToolCard({
|
|||||||
{/* When running: API Key + Model Mappings */}
|
{/* When running: API Key + Model Mappings */}
|
||||||
{isRunning && (
|
{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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
{apiKeys.length > 0 ? (
|
{apiKeys.length > 0 ? (
|
||||||
<select
|
<select
|
||||||
value={selectedApiKey}
|
value={selectedApiKey}
|
||||||
onChange={(e) => setSelectedApiKey(e.target.value)}
|
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>)}
|
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||||
</select>
|
</select>
|
||||||
@@ -341,7 +341,7 @@ export default function AntigravityToolCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tool.defaultModels.map((model) => (
|
{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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
<input
|
<input
|
||||||
@@ -349,12 +349,12 @@ export default function AntigravityToolCard({
|
|||||||
value={modelMappings[model.alias] || ""}
|
value={modelMappings[model.alias] || ""}
|
||||||
onChange={(e) => handleModelMappingChange(model.alias, e.target.value)}
|
onChange={(e) => handleModelMappingChange(model.alias, e.target.value)}
|
||||||
placeholder="provider/model-id"
|
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
|
<button
|
||||||
onClick={() => openModelSelector(model.alias)}
|
onClick={() => openModelSelector(model.alias)}
|
||||||
disabled={!hasActiveProviders}
|
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
|
Select
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default function ClaudeToolCard({
|
|||||||
if (claudeStatus?.installed && !hasInitializedModels.current) {
|
if (claudeStatus?.installed && !hasInitializedModels.current) {
|
||||||
hasInitializedModels.current = true;
|
hasInitializedModels.current = true;
|
||||||
const env = claudeStatus.settings?.env || {};
|
const env = claudeStatus.settings?.env || {};
|
||||||
|
|
||||||
tool.defaultModels.forEach((model) => {
|
tool.defaultModels.forEach((model) => {
|
||||||
if (model.envKey) {
|
if (model.envKey) {
|
||||||
const value = env[model.envKey] || model.defaultValue || "";
|
const value = env[model.envKey] || model.defaultValue || "";
|
||||||
@@ -141,16 +141,16 @@ export default function ClaudeToolCard({
|
|||||||
setMessage(null);
|
setMessage(null);
|
||||||
try {
|
try {
|
||||||
const env = { ANTHROPIC_BASE_URL: getEffectiveBaseUrl() };
|
const env = { ANTHROPIC_BASE_URL: getEffectiveBaseUrl() };
|
||||||
|
|
||||||
// Get key from dropdown, fallback to first key or sk_9router for localhost
|
// 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)
|
|| (apiKeys?.length > 0 ? apiKeys[0].key : null)
|
||||||
|| (!cloudEnabled ? "sk_9router" : null);
|
|| (!cloudEnabled ? "sk_9router" : null);
|
||||||
|
|
||||||
if (keyToUse) {
|
if (keyToUse) {
|
||||||
env.ANTHROPIC_AUTH_TOKEN = keyToUse;
|
env.ANTHROPIC_AUTH_TOKEN = keyToUse;
|
||||||
}
|
}
|
||||||
|
|
||||||
tool.defaultModels.forEach((model) => {
|
tool.defaultModels.forEach((model) => {
|
||||||
const targetModel = modelMappings[model.alias];
|
const targetModel = modelMappings[model.alias];
|
||||||
if (targetModel && model.envKey) env[model.envKey] = targetModel;
|
if (targetModel && model.envKey) env[model.envKey] = targetModel;
|
||||||
@@ -205,15 +205,15 @@ export default function ClaudeToolCard({
|
|||||||
|
|
||||||
// Generate settings.json content for manual copy
|
// Generate settings.json content for manual copy
|
||||||
const getManualConfigs = () => {
|
const getManualConfigs = () => {
|
||||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||||
? selectedApiKey
|
? selectedApiKey
|
||||||
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
||||||
const env = { ANTHROPIC_BASE_URL: getEffectiveBaseUrl(), ANTHROPIC_AUTH_TOKEN: keyToUse };
|
const env = { ANTHROPIC_BASE_URL: getEffectiveBaseUrl(), ANTHROPIC_AUTH_TOKEN: keyToUse };
|
||||||
tool.defaultModels.forEach((model) => {
|
tool.defaultModels.forEach((model) => {
|
||||||
const targetModel = modelMappings[model.alias];
|
const targetModel = modelMappings[model.alias];
|
||||||
if (targetModel && model.envKey) env[model.envKey] = targetModel;
|
if (targetModel && model.envKey) env[model.envKey] = targetModel;
|
||||||
});
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
filename: "~/.claude/settings.json",
|
filename: "~/.claude/settings.json",
|
||||||
@@ -292,7 +292,7 @@ export default function ClaudeToolCard({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{/* Current Base URL */}
|
{/* Current Base URL */}
|
||||||
{claudeStatus?.settings?.env?.ANTHROPIC_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="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="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">
|
<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 */}
|
{/* 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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={getDisplayUrl()}
|
value={getDisplayUrl()}
|
||||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||||
placeholder="https://.../v1"
|
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 && (
|
{customBaseUrl && customBaseUrl !== baseUrl && (
|
||||||
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* API Key */}
|
{/* 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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
{apiKeys.length > 0 ? (
|
{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>)}
|
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
@@ -336,17 +336,17 @@ export default function ClaudeToolCard({
|
|||||||
|
|
||||||
{/* Model Mappings */}
|
{/* Model Mappings */}
|
||||||
{tool.defaultModels.map((model) => (
|
{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="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>
|
<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" />
|
<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={`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>
|
<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>}
|
{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>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* CC Filter Naming */}
|
{/* 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="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>
|
<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">
|
<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}`} />
|
<ModelSelectModal isOpen={modalOpen} onClose={() => setModalOpen(false)} onSelect={handleModelSelect} selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null} activeProviders={activeProviders} modelAliases={modelAliases} title={`Select model for ${currentEditingAlias}`} />
|
||||||
|
|
||||||
<ManualConfigModal
|
<ManualConfigModal
|
||||||
isOpen={showManualConfigModal}
|
isOpen={showManualConfigModal}
|
||||||
onClose={() => setShowManualConfigModal(false)}
|
onClose={() => setShowManualConfigModal(false)}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
|
|||||||
if (codexStatus?.config) {
|
if (codexStatus?.config) {
|
||||||
const modelMatch = codexStatus.config.match(/^model\s*=\s*"([^"]+)"/m);
|
const modelMatch = codexStatus.config.match(/^model\s*=\s*"([^"]+)"/m);
|
||||||
if (modelMatch) setSelectedModel(modelMatch[1]);
|
if (modelMatch) setSelectedModel(modelMatch[1]);
|
||||||
|
|
||||||
// Parse subagent settings
|
// Parse subagent settings
|
||||||
const subagentModelMatch = codexStatus.config.match(/\[agents\.subagent\]\s*\n\s*model\s*=\s*"([^"]+)"/m);
|
const subagentModelMatch = codexStatus.config.match(/\[agents\.subagent\]\s*\n\s*model\s*=\s*"([^"]+)"/m);
|
||||||
if (subagentModelMatch) setSubagentModel(subagentModelMatch[1]);
|
if (subagentModelMatch) setSubagentModel(subagentModelMatch[1]);
|
||||||
@@ -74,7 +74,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
|
|||||||
// Ensure URL ends with /v1
|
// Ensure URL ends with /v1
|
||||||
return url.endsWith("/v1") ? url : `${url}/v1`;
|
return url.endsWith("/v1") ? url : `${url}/v1`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDisplayUrl = () => customBaseUrl || `${baseUrl}/v1`;
|
const getDisplayUrl = () => customBaseUrl || `${baseUrl}/v1`;
|
||||||
|
|
||||||
const checkCodexStatus = async () => {
|
const checkCodexStatus = async () => {
|
||||||
@@ -95,16 +95,16 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
|
|||||||
setMessage(null);
|
setMessage(null);
|
||||||
try {
|
try {
|
||||||
// Use sk_9router for localhost if no key, otherwise use selected key
|
// Use sk_9router for localhost if no key, otherwise use selected key
|
||||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||||
? selectedApiKey
|
? selectedApiKey
|
||||||
: (!cloudEnabled ? "sk_9router" : selectedApiKey);
|
: (!cloudEnabled ? "sk_9router" : selectedApiKey);
|
||||||
|
|
||||||
const res = await fetch("/api/cli-tools/codex-settings", {
|
const res = await fetch("/api/cli-tools/codex-settings", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
baseUrl: getEffectiveBaseUrl(),
|
baseUrl: getEffectiveBaseUrl(),
|
||||||
apiKey: keyToUse,
|
apiKey: keyToUse,
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
subagentModel: subagentModel || selectedModel
|
subagentModel: subagentModel || selectedModel
|
||||||
}),
|
}),
|
||||||
@@ -154,12 +154,12 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getManualConfigs = () => {
|
const getManualConfigs = () => {
|
||||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||||
? selectedApiKey
|
? selectedApiKey
|
||||||
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
||||||
|
|
||||||
const effectiveSubagentModel = subagentModel || selectedModel;
|
const effectiveSubagentModel = subagentModel || selectedModel;
|
||||||
|
|
||||||
const configContent = `# 9Router Configuration for Codex CLI
|
const configContent = `# 9Router Configuration for Codex CLI
|
||||||
model = "${selectedModel}"
|
model = "${selectedModel}"
|
||||||
model_provider = "9router"
|
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>
|
<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">
|
<div className="pt-2 border-t border-border">
|
||||||
<p className="text-text-muted text-xs">
|
<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.
|
Click "Apply" to auto-configure.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -269,7 +269,7 @@ model = "${effectiveSubagentModel}"
|
|||||||
const parsed = codexStatus.config.match(/base_url\s*=\s*"([^"]+)"/);
|
const parsed = codexStatus.config.match(/base_url\s*=\s*"([^"]+)"/);
|
||||||
const currentBaseUrl = parsed ? parsed[1] : null;
|
const currentBaseUrl = parsed ? parsed[1] : null;
|
||||||
return currentBaseUrl ? (
|
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="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="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">
|
<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 */}
|
{/* 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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={getDisplayUrl()}
|
value={getDisplayUrl()}
|
||||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||||
placeholder="https://.../v1"
|
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` && (
|
{customBaseUrl && customBaseUrl !== `${baseUrl}/v1` && (
|
||||||
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* API Key */}
|
{/* 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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
{apiKeys.length > 0 ? (
|
{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>)}
|
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
@@ -313,36 +313,36 @@ model = "${effectiveSubagentModel}"
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Model */}
|
{/* 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="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>
|
<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" />
|
<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={`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>
|
<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>}
|
{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>
|
||||||
|
|
||||||
{/* Subagent Model */}
|
{/* 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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={subagentModel}
|
value={subagentModel}
|
||||||
onChange={(e) => setSubagentModel(e.target.value)}
|
onChange={(e) => setSubagentModel(e.target.value)}
|
||||||
placeholder={selectedModel || "provider/model-id (defaults to main model)"}
|
placeholder={selectedModel || "provider/model-id (defaults to main model)"}
|
||||||
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSubagentModalOpen(true)}
|
onClick={() => setSubagentModalOpen(true)}
|
||||||
disabled={!activeProviders?.length}
|
disabled={!activeProviders?.length}
|
||||||
className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
className={`w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||||
>
|
>
|
||||||
Select Model
|
Select Model
|
||||||
</button>
|
</button>
|
||||||
{subagentModel && (
|
{subagentModel && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setSubagentModel("")}
|
onClick={() => setSubagentModel("")}
|
||||||
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||||
title="Clear (will use main model)"
|
title="Clear (will use main model)"
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-[14px]">close</span>
|
<span className="material-symbols-outlined text-[14px]">close</span>
|
||||||
|
|||||||
@@ -48,13 +48,13 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
|
|||||||
|
|
||||||
const renderApiKeySelector = () => {
|
const renderApiKeySelector = () => {
|
||||||
return (
|
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 ? (
|
{apiKeys && apiKeys.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<select
|
<select
|
||||||
value={selectedApiKey}
|
value={selectedApiKey}
|
||||||
onChange={(e) => setSelectedApiKey(e.target.value)}
|
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) => (
|
{apiKeys.map((key) => (
|
||||||
<option key={key.id} value={key.key}>{key.key}</option>
|
<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 = () => {
|
const renderModelSelector = () => {
|
||||||
return (
|
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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={modelValue}
|
value={modelValue}
|
||||||
onChange={(e) => setModelValue(e.target.value)}
|
onChange={(e) => setModelValue(e.target.value)}
|
||||||
placeholder="provider/model-id"
|
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
|
<button
|
||||||
onClick={() => setShowModelModal(true)}
|
onClick={() => setShowModelModal(true)}
|
||||||
@@ -188,8 +188,8 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
|
|||||||
{item.type === "apiKeySelector" && renderApiKeySelector()}
|
{item.type === "apiKeySelector" && renderApiKeySelector()}
|
||||||
{item.type === "modelSelector" && renderModelSelector()}
|
{item.type === "modelSelector" && renderModelSelector()}
|
||||||
{item.value && (
|
{item.value && (
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-2 flex flex-col sm:flex-row sm: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">
|
<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)}
|
{replaceVars(item.value)}
|
||||||
</code>
|
</code>
|
||||||
{item.copyable && (
|
{item.copyable && (
|
||||||
@@ -262,7 +262,7 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="size-8 rounded-lg flex items-center justify-center shrink-0">
|
<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">
|
<div className="flex flex-col gap-2">
|
||||||
{/* Current Base URL */}
|
{/* Current Base URL */}
|
||||||
{droidStatus?.settings?.customModels?.find(m => m.id?.startsWith("custom:9Router"))?.baseUrl && (
|
{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="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="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">
|
<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 */}
|
{/* 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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
<input
|
<input
|
||||||
@@ -305,7 +305,7 @@ export default function DroidToolCard({
|
|||||||
value={getDisplayUrl()}
|
value={getDisplayUrl()}
|
||||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||||
placeholder="https://.../v1"
|
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 && (
|
{customBaseUrl && customBaseUrl !== baseUrl && (
|
||||||
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* API Key */}
|
{/* 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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
{apiKeys.length > 0 ? (
|
{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>)}
|
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
@@ -330,7 +330,7 @@ export default function DroidToolCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Models */}
|
{/* 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">
|
<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>}
|
Models {modelList.length > 0 && <span className="text-primary">({modelList.length})</span>}
|
||||||
</span>
|
</span>
|
||||||
@@ -357,7 +357,7 @@ export default function DroidToolCard({
|
|||||||
onChange={(e) => setModelInput(e.target.value)}
|
onChange={(e) => setModelInput(e.target.value)}
|
||||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addModel(); } }}
|
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addModel(); } }}
|
||||||
placeholder="provider/model-id"
|
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
|
<button
|
||||||
onClick={() => setModalOpen(true)}
|
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>
|
<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>
|
</div>
|
||||||
<div className="flex items-center gap-2 pl-9">
|
<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="!bg-yellow-500/20 !border-yellow-500/40 !text-yellow-700 dark:!text-yellow-300 hover:!bg-yellow-500/30">
|
<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>
|
<span className="material-symbols-outlined text-[18px] mr-1">content_copy</span>
|
||||||
Manual Config
|
Manual Config
|
||||||
</Button>
|
</Button>
|
||||||
@@ -228,7 +228,7 @@ export default function HermesToolCard({
|
|||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{hermesStatus?.settings?.model?.base_url && (
|
{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="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="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">
|
<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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
<input
|
<input
|
||||||
@@ -245,7 +245,7 @@ export default function HermesToolCard({
|
|||||||
value={getEffectiveBaseUrl()}
|
value={getEffectiveBaseUrl()}
|
||||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||||
placeholder="https://.../v1"
|
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 && (
|
{customBaseUrl && customBaseUrl !== baseUrl && (
|
||||||
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
|
<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>
|
||||||
|
|
||||||
<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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
{apiKeys.length > 0 ? (
|
{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>)}
|
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
@@ -268,11 +268,11 @@ export default function HermesToolCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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="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>
|
<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" />
|
<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={`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={() => 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>}
|
{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>
|
||||||
</div>
|
</div>
|
||||||
@@ -284,14 +284,14 @@ export default function HermesToolCard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
<Button variant="primary" size="sm" onClick={handleApply} disabled={!selectedModel} loading={applying}>
|
<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
|
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
|
||||||
</Button>
|
</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
|
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>Reset
|
||||||
</Button>
|
</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
|
<span className="material-symbols-outlined text-[14px] mr-1">content_copy</span>Manual Config
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export default function MitmToolCard({
|
|||||||
{tool.defaultModels?.length > 0 && (
|
{tool.defaultModels?.length > 0 && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{tool.defaultModels.map((model) => (
|
{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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -127,15 +127,15 @@ export default function OpenClawToolCard({
|
|||||||
setApplying(true);
|
setApplying(true);
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
try {
|
try {
|
||||||
const keyToUse = selectedApiKey?.trim()
|
const keyToUse = selectedApiKey?.trim()
|
||||||
|| (apiKeys?.length > 0 ? apiKeys[0].key : null)
|
|| (apiKeys?.length > 0 ? apiKeys[0].key : null)
|
||||||
|| (!cloudEnabled ? "sk_9router" : null);
|
|| (!cloudEnabled ? "sk_9router" : null);
|
||||||
|
|
||||||
const res = await fetch("/api/cli-tools/openclaw-settings", {
|
const res = await fetch("/api/cli-tools/openclaw-settings", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
baseUrl: getEffectiveBaseUrl(),
|
baseUrl: getEffectiveBaseUrl(),
|
||||||
apiKey: keyToUse,
|
apiKey: keyToUse,
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
agentModels,
|
agentModels,
|
||||||
@@ -187,8 +187,8 @@ export default function OpenClawToolCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getManualConfigs = () => {
|
const getManualConfigs = () => {
|
||||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||||
? selectedApiKey
|
? selectedApiKey
|
||||||
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
||||||
|
|
||||||
const settingsContent = {
|
const settingsContent = {
|
||||||
@@ -278,7 +278,7 @@ export default function OpenClawToolCard({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{/* Current Base URL */}
|
{/* Current Base URL */}
|
||||||
{openclawStatus?.settings?.models?.providers?.["9router"]?.baseUrl && (
|
{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="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="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">
|
<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 */}
|
{/* 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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={getDisplayUrl()}
|
value={getDisplayUrl()}
|
||||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||||
placeholder="https://.../v1"
|
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 && (
|
{customBaseUrl && customBaseUrl !== baseUrl && (
|
||||||
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* API Key */}
|
{/* 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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
{apiKeys.length > 0 ? (
|
{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>)}
|
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
@@ -321,11 +321,11 @@ export default function OpenClawToolCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Default Model */}
|
{/* 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="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>
|
<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" />
|
<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={`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(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>}
|
{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>
|
||||||
|
|
||||||
@@ -339,9 +339,9 @@ export default function OpenClawToolCard({
|
|||||||
value={agentModels[agent.id] || ""}
|
value={agentModels[agent.id] || ""}
|
||||||
onChange={(e) => setAgentModels(prev => ({ ...prev, [agent.id]: e.target.value }))}
|
onChange={(e) => setAgentModels(prev => ({ ...prev, [agent.id]: e.target.value }))}
|
||||||
placeholder={`default (${selectedModel || "provider/model-id"})`}
|
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>}
|
{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>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
|||||||
if (status?.opencode?.activeModel) {
|
if (status?.opencode?.activeModel) {
|
||||||
setActiveModel(status.opencode.activeModel);
|
setActiveModel(status.opencode.activeModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse subagent settings from agent.explorer if exists
|
// Parse subagent settings from agent.explorer if exists
|
||||||
if (status?.config?.agent?.explorer?.model?.startsWith("9router/")) {
|
if (status?.config?.agent?.explorer?.model?.startsWith("9router/")) {
|
||||||
setSubagentModel(status.config.agent.explorer.model.replace("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", {
|
const res = await fetch("/api/cli-tools/opencode-settings", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
baseUrl: getEffectiveBaseUrl(),
|
baseUrl: getEffectiveBaseUrl(),
|
||||||
apiKey: keyToUse,
|
apiKey: keyToUse,
|
||||||
models: selectedModels,
|
models: selectedModels,
|
||||||
activeModel: activeModel === "" ? "" : (activeModel || selectedModels[0]),
|
activeModel: activeModel === "" ? "" : (activeModel || selectedModels[0]),
|
||||||
subagentModel: subagentModel
|
subagentModel: subagentModel
|
||||||
@@ -257,7 +257,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{/* Current base URL */}
|
{/* Current base URL */}
|
||||||
{status?.config?.provider?.["9router"]?.options?.baseURL && (
|
{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="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="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">
|
<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 */}
|
{/* 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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
<input
|
<input
|
||||||
@@ -275,7 +275,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
|||||||
value={getDisplayUrl()}
|
value={getDisplayUrl()}
|
||||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||||
placeholder="https://.../v1"
|
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` && (
|
{customBaseUrl && customBaseUrl !== `${baseUrl}/v1` && (
|
||||||
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* API Key */}
|
{/* 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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
{apiKeys.length > 0 ? (
|
{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>)}
|
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||||
</select>
|
</select>
|
||||||
) : (
|
) : (
|
||||||
@@ -300,7 +300,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Models */}
|
{/* 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="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>
|
<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">
|
<div className="flex-1 flex flex-col gap-2">
|
||||||
@@ -364,7 +364,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
|||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
<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">
|
<span className="text-xs text-text-muted">
|
||||||
{selectedModels.length > 0 && activeModel ? (
|
{selectedModels.length > 0 && activeModel ? (
|
||||||
@@ -380,27 +380,27 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subagent Model */}
|
{/* 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="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>
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={subagentModel}
|
value={subagentModel}
|
||||||
onChange={(e) => setSubagentModel(e.target.value)}
|
onChange={(e) => setSubagentModel(e.target.value)}
|
||||||
placeholder={selectedModel || "provider/model-id (defaults to main model)"}
|
placeholder={selectedModel || "provider/model-id (defaults to main model)"}
|
||||||
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSubagentModalOpen(true)}
|
onClick={() => setSubagentModalOpen(true)}
|
||||||
disabled={!activeProviders?.length}
|
disabled={!activeProviders?.length}
|
||||||
className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
className={`w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||||
>
|
>
|
||||||
Select Model
|
Select Model
|
||||||
</button>
|
</button>
|
||||||
{subagentModel && (
|
{subagentModel && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setSubagentModel("")}
|
onClick={() => setSubagentModel("")}
|
||||||
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||||
title="Clear (will use main model)"
|
title="Clear (will use main model)"
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined text-[14px]">close</span>
|
<span className="material-symbols-outlined text-[14px]">close</span>
|
||||||
@@ -435,7 +435,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
|||||||
<ModelSelectModal
|
<ModelSelectModal
|
||||||
isOpen={modalOpen}
|
isOpen={modalOpen}
|
||||||
onClose={() => setModalOpen(false)}
|
onClose={() => setModalOpen(false)}
|
||||||
onSelect={(model) => {
|
onSelect={(model) => {
|
||||||
if (!selectedModels.includes(model.value)) {
|
if (!selectedModels.includes(model.value)) {
|
||||||
setSelectedModels([...selectedModels, model.value]);
|
setSelectedModels([...selectedModels, model.value]);
|
||||||
if (!activeModel) setActiveModel(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
|
// Shared row layout — defined outside components to avoid re-mount on re-render
|
||||||
function Row({ label, children }) {
|
function Row({ label, children }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex min-w-0 flex-col gap-1.5 sm:flex-row sm:items-center sm:gap-3">
|
||||||
<span className="text-xs text-text-muted w-20 shrink-0">{label}</span>
|
<span className="w-full text-xs font-medium text-text-muted sm:w-20 sm:shrink-0">{label}</span>
|
||||||
<div className="flex-1">{children}</div>
|
<div className="w-full min-w-0 flex-1">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -230,11 +230,11 @@ function EmbeddingExampleCard({ providerId, customAlias }) {
|
|||||||
|
|
||||||
{/* Endpoint */}
|
{/* Endpoint */}
|
||||||
<Row label="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
|
<input
|
||||||
value={endpoint}
|
value={endpoint}
|
||||||
onChange={(e) => useTunnel ? setTunnelEndpoint(e.target.value) : setLocalEndpoint(e.target.value)}
|
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"
|
placeholder="http://localhost:3000"
|
||||||
/>
|
/>
|
||||||
{/* Tunnel toggle — only show if tunnel URL is available */}
|
{/* Tunnel toggle — only show if tunnel URL is available */}
|
||||||
@@ -298,12 +298,12 @@ function EmbeddingExampleCard({ providerId, customAlias }) {
|
|||||||
|
|
||||||
{/* Curl + Run */}
|
{/* Curl + Run */}
|
||||||
<div className="mt-1">
|
<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>
|
<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
|
<button
|
||||||
onClick={() => copyCurl(curlSnippet)}
|
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>
|
<span className="material-symbols-outlined text-[14px]">{copiedCurl ? "check" : "content_copy"}</span>
|
||||||
{copiedCurl ? "Copied" : "Copy"}
|
{copiedCurl ? "Copied" : "Copy"}
|
||||||
@@ -311,7 +311,7 @@ function EmbeddingExampleCard({ providerId, customAlias }) {
|
|||||||
<button
|
<button
|
||||||
onClick={handleRun}
|
onClick={handleRun}
|
||||||
disabled={running || !input.trim() || !modelFull}
|
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}>
|
<span className="material-symbols-outlined text-[14px]" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
|
||||||
play_arrow
|
play_arrow
|
||||||
@@ -320,7 +320,7 @@ function EmbeddingExampleCard({ providerId, customAlias }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
@@ -328,21 +328,21 @@ function EmbeddingExampleCard({ providerId, customAlias }) {
|
|||||||
|
|
||||||
{/* Response — default example or real result */}
|
{/* Response — default example or real result */}
|
||||||
<div>
|
<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">
|
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">
|
||||||
Response {result && <span className="font-normal normal-case">⚡ {result.latencyMs}ms</span>}
|
Response {result && <span className="font-normal normal-case">⚡ {result.latencyMs}ms</span>}
|
||||||
</span>
|
</span>
|
||||||
{result && (
|
{result && (
|
||||||
<button
|
<button
|
||||||
onClick={() => copyRes(resultJson)}
|
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>
|
<span className="material-symbols-outlined text-[14px]">{copiedRes ? "check" : "content_copy"}</span>
|
||||||
{copiedRes ? "Copied" : "Copy"}
|
{copiedRes ? "Copied" : "Copy"}
|
||||||
</button>
|
</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 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)}
|
{formatResultJson(result?.data)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -567,8 +567,8 @@ function TtsExampleCard({ providerId }) {
|
|||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex flex-col gap-2.5">
|
||||||
{/* Endpoint + API Key as read-only text */}
|
{/* Endpoint + API Key as read-only text */}
|
||||||
<Row label="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">
|
||||||
<span className="flex-1 px-3 py-1.5 text-sm font-mono text-text-main bg-sidebar rounded-lg truncate">
|
<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
|
{endpoint}/v1/audio/speech
|
||||||
</span>
|
</span>
|
||||||
{tunnelEndpoint && (
|
{tunnelEndpoint && (
|
||||||
@@ -611,10 +611,10 @@ function TtsExampleCard({ providerId }) {
|
|||||||
{/* Language row + Browse button (edge-tts, local-device, elevenlabs) */}
|
{/* Language row + Browse button (edge-tts, local-device, elevenlabs) */}
|
||||||
{config.hasBrowseButton && (
|
{config.hasBrowseButton && (
|
||||||
<Row label="Language">
|
<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
|
<button
|
||||||
onClick={openModal}
|
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
|
{selectedLang
|
||||||
? <span className="text-text-main">{languages.find((l) => l.code === selectedLang)?.name || selectedLang}</span>
|
? <span className="text-text-main">{languages.find((l) => l.code === selectedLang)?.name || selectedLang}</span>
|
||||||
@@ -622,7 +622,7 @@ function TtsExampleCard({ providerId }) {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={openModal}
|
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>
|
<span className="material-symbols-outlined text-[14px]">language</span>
|
||||||
Select language
|
Select language
|
||||||
@@ -743,12 +743,12 @@ function TtsExampleCard({ providerId }) {
|
|||||||
|
|
||||||
{/* Curl + Run */}
|
{/* Curl + Run */}
|
||||||
<div className="mt-1">
|
<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>
|
<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
|
<button
|
||||||
onClick={() => copyCurl(curlSnippet)}
|
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>
|
<span className="material-symbols-outlined text-[14px]">{copiedCurl ? "check" : "content_copy"}</span>
|
||||||
{copiedCurl ? "Copied" : "Copy"}
|
{copiedCurl ? "Copied" : "Copy"}
|
||||||
@@ -756,7 +756,7 @@ function TtsExampleCard({ providerId }) {
|
|||||||
<button
|
<button
|
||||||
onClick={handleRun}
|
onClick={handleRun}
|
||||||
disabled={running || !input.trim() || !modelFull}
|
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}>
|
<span className="material-symbols-outlined text-[14px]" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
|
||||||
play_arrow
|
play_arrow
|
||||||
@@ -765,7 +765,7 @@ function TtsExampleCard({ providerId }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-xs text-red-500 break-words">{error}</p>}
|
{error && <p className="text-xs text-red-500 break-words">{error}</p>}
|
||||||
@@ -773,11 +773,11 @@ function TtsExampleCard({ providerId }) {
|
|||||||
{/* Audio player */}
|
{/* Audio player */}
|
||||||
{audioUrl ? (
|
{audioUrl ? (
|
||||||
<div>
|
<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">
|
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">
|
||||||
Response {latency && <span className="font-normal normal-case">⚡ {latency}ms</span>}
|
Response {latency && <span className="font-normal normal-case">⚡ {latency}ms</span>}
|
||||||
</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>
|
<span className="material-symbols-outlined text-[14px]">download</span>
|
||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
@@ -787,7 +787,7 @@ function TtsExampleCard({ providerId }) {
|
|||||||
{/* JSON Response (if format is json) */}
|
{/* JSON Response (if format is json) */}
|
||||||
{jsonResponse && (
|
{jsonResponse && (
|
||||||
<div className="mt-3">
|
<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>
|
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">JSON Response</span>
|
||||||
</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-wrap break-all">
|
<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>
|
<div>
|
||||||
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">Response</span>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -811,7 +811,7 @@ function TtsExampleCard({ providerId }) {
|
|||||||
{/* Country Picker Modal */}
|
{/* Country Picker Modal */}
|
||||||
{modalOpen && (
|
{modalOpen && (
|
||||||
<div
|
<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)" }}
|
style={{ backgroundColor: "rgba(0,0,0,0.6)", backdropFilter: "blur(2px)" }}
|
||||||
onClick={() => setModalOpen(false)}
|
onClick={() => setModalOpen(false)}
|
||||||
>
|
>
|
||||||
@@ -1077,8 +1077,8 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
|
|
||||||
{/* Endpoint */}
|
{/* Endpoint */}
|
||||||
<Row label="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">
|
||||||
<span className="flex-1 px-3 py-1.5 text-sm font-mono text-text-main bg-sidebar rounded-lg truncate">
|
<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}
|
{endpoint}{apiPath}
|
||||||
</span>
|
</span>
|
||||||
{tunnelEndpoint && (
|
{tunnelEndpoint && (
|
||||||
@@ -1232,12 +1232,12 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
|
|
||||||
{/* Curl + Run */}
|
{/* Curl + Run */}
|
||||||
<div className="mt-1">
|
<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>
|
<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
|
<button
|
||||||
onClick={() => copyCurl(curlSnippet)}
|
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>
|
<span className="material-symbols-outlined text-[14px]">{copiedCurl ? "check" : "content_copy"}</span>
|
||||||
{copiedCurl ? "Copied" : "Copy"}
|
{copiedCurl ? "Copied" : "Copy"}
|
||||||
@@ -1245,7 +1245,7 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
<button
|
<button
|
||||||
onClick={handleRun}
|
onClick={handleRun}
|
||||||
disabled={running || !input.trim() || !modelFull}
|
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}>
|
<span className="material-symbols-outlined text-[14px]" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
|
||||||
play_arrow
|
play_arrow
|
||||||
@@ -1254,12 +1254,12 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Streaming progress */}
|
{/* Streaming progress */}
|
||||||
{(running || progress) && useStreaming && (
|
{(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}>
|
<span className="material-symbols-outlined text-[16px] text-primary" style={running ? { animation: "spin 1s linear infinite" } : undefined}>
|
||||||
{running ? "progress_activity" : "check_circle"}
|
{running ? "progress_activity" : "check_circle"}
|
||||||
</span>
|
</span>
|
||||||
@@ -1287,21 +1287,21 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
|
|
||||||
{/* Response */}
|
{/* Response */}
|
||||||
<div>
|
<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">
|
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">
|
||||||
Response {result && <span className="font-normal normal-case">⚡ {result.latencyMs}ms</span>}
|
Response {result && <span className="font-normal normal-case">⚡ {result.latencyMs}ms</span>}
|
||||||
</span>
|
</span>
|
||||||
{result && (
|
{result && (
|
||||||
<button
|
<button
|
||||||
onClick={() => copyRes(resultJson)}
|
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>
|
<span className="material-symbols-outlined text-[14px]">{copiedRes ? "check" : "content_copy"}</span>
|
||||||
{copiedRes ? "Copied" : "Copy"}
|
{copiedRes ? "Copied" : "Copy"}
|
||||||
</button>
|
</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 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}
|
{result ? resultJson : exConfig.defaultResponse}
|
||||||
</pre>
|
</pre>
|
||||||
{kind === "image" && (binaryImageUrl || result?.data?.data?.[0]) && (
|
{kind === "image" && (binaryImageUrl || result?.data?.data?.[0]) && (
|
||||||
@@ -1310,7 +1310,7 @@ function GenericExampleCard({ providerId, kind }) {
|
|||||||
<a
|
<a
|
||||||
href={binaryImageUrl || (result?.data?.data?.[0]?.b64_json ? `data:image/png;base64,${result.data.data[0].b64_json}` : result?.data?.data?.[0]?.url || "")}
|
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"
|
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>
|
<span className="material-symbols-outlined text-[14px]">download</span>
|
||||||
Download
|
Download
|
||||||
@@ -1396,7 +1396,7 @@ export default function MediaProviderDetailPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Header */}
|
{/* 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` }}>
|
<div className="size-12 rounded-lg flex items-center justify-center shrink-0" style={{ backgroundColor: `${provider.color}15` }}>
|
||||||
<ProviderIcon
|
<ProviderIcon
|
||||||
src={`/providers/${provider.id}.png`}
|
src={`/providers/${provider.id}.png`}
|
||||||
@@ -1408,7 +1408,7 @@ export default function MediaProviderDetailPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<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>
|
<h1 className="text-3xl font-semibold tracking-tight">{provider.name}</h1>
|
||||||
{!isCustom && provider.notice?.apiKeyUrl && (
|
{!isCustom && provider.notice?.apiKeyUrl && (
|
||||||
<a
|
<a
|
||||||
@@ -1432,7 +1432,7 @@ export default function MediaProviderDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isCustom && (
|
{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)}>
|
<Button size="sm" variant="secondary" icon="edit" onClick={() => setShowEditModal(true)}>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function MediaProviderCard({ provider, kind, connections, isCustom }) {
|
|||||||
padding="xs"
|
padding="xs"
|
||||||
className={`h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer ${allDisabled ? "opacity-50" : ""}`}
|
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
|
<div
|
||||||
className="size-8 rounded-lg flex items-center justify-center shrink-0"
|
className="size-8 rounded-lg flex items-center justify-center shrink-0"
|
||||||
style={{ backgroundColor: `${provider.color?.length > 7 ? provider.color : (provider.color ?? "#888") + "15"}` }}
|
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.
|
No connected providers available. Add a connection first in the {KIND_LABELS[kind]} section.
|
||||||
</div>
|
</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) => {
|
{providers.map((p) => {
|
||||||
const already = currentIds.includes(p.id);
|
const already = currentIds.includes(p.id);
|
||||||
return (
|
return (
|
||||||
@@ -218,7 +218,7 @@ export default function ComboDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{/* Header */}
|
{/* 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">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<Link href="/dashboard/media-providers/web" className="text-text-muted hover:text-primary">
|
<Link href="/dashboard/media-providers/web" className="text-text-muted hover:text-primary">
|
||||||
<span className="material-symbols-outlined">arrow_back</span>
|
<span className="material-symbols-outlined">arrow_back</span>
|
||||||
@@ -256,7 +256,7 @@ export default function ComboDetailPage() {
|
|||||||
|
|
||||||
{/* Providers Card */}
|
{/* Providers Card */}
|
||||||
<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>
|
<div>
|
||||||
<h2 className="text-lg font-semibold">Providers</h2>
|
<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>
|
<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 */}
|
{/* Test Example Card */}
|
||||||
{combo.kind && (
|
{combo.kind && (
|
||||||
<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">
|
||||||
<h2 className="text-lg font-semibold">Test Example</h2>
|
<h2 className="text-lg font-semibold">Test Example</h2>
|
||||||
<Button size="sm" icon="play_arrow" onClick={handleTest} disabled={testing || providers.length === 0}>
|
<Button size="sm" icon="play_arrow" onClick={handleTest} disabled={testing || providers.length === 0}>
|
||||||
{testing ? "Running..." : "Run"}
|
{testing ? "Running..." : "Run"}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function ProviderCard({ provider, kind, connections }) {
|
|||||||
return (
|
return (
|
||||||
<Link href={`/dashboard/media-providers/${kind}/${provider.id}`} className="group">
|
<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" : ""}`}>
|
<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
|
<div
|
||||||
className="size-8 rounded-lg flex items-center justify-center shrink-0"
|
className="size-8 rounded-lg flex items-center justify-center shrink-0"
|
||||||
style={{ backgroundColor: `${provider.color?.length > 7 ? provider.color : (provider.color ?? "#888") + "15"}` }}
|
style={{ backgroundColor: `${provider.color?.length > 7 ? provider.color : (provider.color ?? "#888") + "15"}` }}
|
||||||
@@ -72,11 +72,11 @@ function ComboList({ combos }) {
|
|||||||
{combos.map((combo) => (
|
{combos.map((combo) => (
|
||||||
<Link key={combo.id} href={`/dashboard/media-providers/web/combo/${combo.id}`}>
|
<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">
|
<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>
|
<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>
|
<code className="text-sm font-mono font-medium flex-1 truncate">{combo.name}</code>
|
||||||
{/* Provider icons preview */}
|
{/* 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) => {
|
{combo.models.slice(0, 6).map((pid, i) => {
|
||||||
const p = AI_PROVIDERS[pid];
|
const p = AI_PROVIDERS[pid];
|
||||||
return (
|
return (
|
||||||
@@ -110,8 +110,8 @@ function Section({ title, icon, kind, providers, connections, combos, onCreateCo
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Header — title left, Create Combo right */}
|
{/* Header — title left, Create Combo right */}
|
||||||
<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 className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="material-symbols-outlined text-primary">{icon}</span>
|
<span className="material-symbols-outlined text-primary">{icon}</span>
|
||||||
<h2 className="text-base font-semibold">{title}</h2>
|
<h2 className="text-base font-semibold">{title}</h2>
|
||||||
<span className="text-xs text-text-muted">({providers.length} providers · {combos.length} combos)</span>
|
<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 (
|
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={`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 items-center gap-3 flex-1 min-w-0">
|
<div className="flex w-full min-w-0 flex-1 items-start gap-3 sm:items-center">
|
||||||
<div className="flex flex-col">
|
<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"}`}>
|
<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>
|
<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>
|
<span className="material-symbols-outlined text-base text-text-muted">{isOAuth ? "lock" : "key"}</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium truncate">{displayName}</p>
|
<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>
|
<Badge variant={getStatusVariant()} size="sm" dot>
|
||||||
{connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")}
|
{connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -129,7 +129,7 @@ function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMov
|
|||||||
<span className="text-xs text-text-muted">#{connection.priority}</span>
|
<span className="text-xs text-text-muted">#{connection.priority}</span>
|
||||||
</div>
|
</div>
|
||||||
{hasAnyProxy && (
|
{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>
|
<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>}
|
{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>}
|
{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>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex w-full flex-wrap items-center justify-between gap-2 sm:w-auto sm:justify-end">
|
||||||
<div className="flex gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{(proxyPools || []).length > 0 && (
|
{(proxyPools || []).length > 0 && (
|
||||||
<div className="relative" ref={proxyDropdownRef}>
|
<div className="relative" ref={proxyDropdownRef}>
|
||||||
<button
|
<button
|
||||||
@@ -398,9 +398,9 @@ export default function ConnectionsCard({ providerId, isOAuth }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card>
|
<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>
|
<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>
|
<span className="text-xs text-text-muted font-medium">Round Robin</span>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={providerStrategy === "round-robin"}
|
checked={providerStrategy === "round-robin"}
|
||||||
@@ -412,12 +412,12 @@ export default function ConnectionsCard({ providerId, isOAuth }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{providerStrategy === "round-robin" && (
|
{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>
|
<span className="text-xs text-text-muted">Sticky:</span>
|
||||||
<input
|
<input
|
||||||
type="number" min={1} value={providerStickyLimit}
|
type="number" min={1} value={providerStickyLimit}
|
||||||
onChange={(e) => { setProviderStickyLimit(e.target.value); saveStrategy("round-robin", e.target.value); }}
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -425,7 +425,7 @@ export default function ConnectionsCard({ providerId, isOAuth }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{connections.length === 0 ? (
|
{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>
|
<p className="text-sm text-text-muted">No connections yet</p>
|
||||||
<Button size="sm" icon="add" onClick={() => setShowAddModal(true)}>Add Connection</Button>
|
<Button size="sm" icon="add" onClick={() => setShowAddModal(true)}>Add Connection</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -449,7 +449,7 @@ export default function ConnectionsCard({ providerId, isOAuth }) {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
<Button size="sm" icon="add" onClick={() => setShowAddModal(true)}>Add</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const navItems = [
|
|||||||
// { href: "/dashboard/basic-chat", label: "Basic Chat", icon: "chat" }, // Hidden
|
// { href: "/dashboard/basic-chat", label: "Basic Chat", icon: "chat" }, // Hidden
|
||||||
{ href: "/dashboard/combos", label: "Combos", icon: "layers" },
|
{ href: "/dashboard/combos", label: "Combos", icon: "layers" },
|
||||||
{ href: "/dashboard/usage", label: "Usage", icon: "bar_chart" },
|
{ 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/quota", label: "Quota Tracker", icon: "data_usage" },
|
||||||
{ href: "/dashboard/mitm", label: "MITM", icon: "security" },
|
{ href: "/dashboard/mitm", label: "MITM", icon: "security" },
|
||||||
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
|
{ 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" } },
|
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" } },
|
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 }] } },
|
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"] },
|
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