gitpilot / frontend /components /LlmSettings.jsx
github-actions[bot]
Deploy from f861358f
91bfec6
import React, { useEffect, useMemo, useState } from "react";
import { testProvider } from "../utils/api";
const PROVIDERS = ["ollabridge", "openai", "claude", "watsonx", "ollama"];
const PROVIDER_LABELS = {
ollabridge: "OllaBridge Cloud",
openai: "OpenAI",
claude: "Claude",
watsonx: "Watsonx",
ollama: "Ollama",
};
const AUTH_MODES = [
{ id: "device", label: "Device Pairing", icon: "📱" },
{ id: "apikey", label: "API Key", icon: "🔑" },
{ id: "local", label: "Local Trust", icon: "🏠" },
];
function LoadingState({ loadingMessage, loadingSlow, onRetry }) {
return (
<div className="settings-loading-shell">
<div className="settings-loading-card">
<div className="settings-loading-spinner" aria-hidden="true" />
<h1>AI Providers</h1>
<div className="settings-loading-subtitle">Admin / LLM Settings</div>
<p className="settings-loading-text">{loadingMessage}</p>
{loadingSlow && (
<div className="settings-loading-slow">
<p>
This is taking longer than expected. The backend may still be
starting or the settings endpoint may be slow.
</p>
<button
type="button"
className="settings-secondary-btn"
onClick={onRetry}
>
Retry
</button>
</div>
)}
</div>
</div>
);
}
export default function LlmSettings() {
const [settings, setSettings] = useState(null);
const [initialLoading, setInitialLoading] = useState(true);
const [loadingSlow, setLoadingSlow] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const [savedMsg, setSavedMsg] = useState("");
const [modelsByProvider, setModelsByProvider] = useState({});
const [modelsError, setModelsError] = useState("");
const [loadingModelsFor, setLoadingModelsFor] = useState("");
const [testResult, setTestResult] = useState(null);
const [testing, setTesting] = useState(false);
const [authMode, setAuthMode] = useState("local");
const [pairCode, setPairCode] = useState("");
const [pairing, setPairing] = useState(false);
const [pairResult, setPairResult] = useState(null);
const loadingMessage = useMemo(() => {
if (loadingSlow) {
return "Still loading provider configuration…";
}
return "Loading current configuration…";
}, [loadingSlow]);
const loadSettings = async () => {
setInitialLoading(true);
setError("");
setLoadingSlow(false);
let slowTimer;
try {
slowTimer = window.setTimeout(() => {
setLoadingSlow(true);
}, 1500);
const res = await fetch("/api/settings");
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Failed to load settings");
}
setSettings(data);
} catch (e) {
console.error(e);
setError(e.message || "Failed to load settings");
} finally {
window.clearTimeout(slowTimer);
setInitialLoading(false);
}
};
useEffect(() => {
loadSettings();
}, []);
const updateField = (section, field, value) => {
setSettings((prev) => ({
...prev,
[section]: {
...prev[section],
[field]: value,
},
}));
};
const handleSave = async () => {
setSaving(true);
setError("");
setSavedMsg("");
try {
const res = await fetch("/api/settings/llm", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(settings),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Failed to save settings");
setSettings(data);
setSavedMsg("Settings saved successfully!");
setTimeout(() => setSavedMsg(""), 3000);
} catch (e) {
console.error(e);
setError(e.message || "Failed to save settings");
} finally {
setSaving(false);
}
};
const loadModelsForProvider = async (provider) => {
setModelsError("");
setLoadingModelsFor(provider);
try {
const res = await fetch(`/api/settings/models?provider=${provider}`);
const data = await res.json();
if (!res.ok || data.error) {
throw new Error(data.error || "Failed to load models");
}
setModelsByProvider((prev) => ({
...prev,
[provider]: data.models || [],
}));
} catch (e) {
console.error(e);
setModelsError(e.message || "Failed to load models");
} finally {
setLoadingModelsFor("");
}
};
const handlePair = async () => {
if (!pairCode.trim()) return;
setPairing(true);
setPairResult(null);
try {
const baseUrl =
settings?.ollabridge?.base_url || "https://ruslanmv-ollabridge.hf.space";
const res = await fetch("/api/ollabridge/pair", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ base_url: baseUrl, code: pairCode.trim() }),
});
const data = await res.json();
if (data.success) {
setPairResult({ ok: true, message: "Paired successfully!" });
if (data.token) {
updateField("ollabridge", "api_key", data.token);
}
} else {
setPairResult({
ok: false,
message: data.error || "Pairing failed",
});
}
} catch (e) {
setPairResult({ ok: false, message: e.message || "Pairing failed" });
} finally {
setPairing(false);
}
};
const handleTestConnection = async () => {
setTesting(true);
setTestResult(null);
try {
const activeProvider = settings?.provider || "ollama";
const config = { provider: activeProvider };
if (activeProvider === "openai" && settings?.openai) {
config.openai = {
api_key: settings.openai.api_key,
base_url: settings.openai.base_url,
model: settings.openai.model,
};
} else if (activeProvider === "claude" && settings?.claude) {
config.claude = {
api_key: settings.claude.api_key,
base_url: settings.claude.base_url,
model: settings.claude.model,
};
} else if (activeProvider === "watsonx" && settings?.watsonx) {
config.watsonx = {
api_key: settings.watsonx.api_key,
project_id: settings.watsonx.project_id,
base_url: settings.watsonx.base_url,
model_id: settings.watsonx.model_id,
};
} else if (activeProvider === "ollama" && settings?.ollama) {
config.ollama = {
base_url: settings.ollama.base_url,
model: settings.ollama.model,
};
} else if (activeProvider === "ollabridge" && settings?.ollabridge) {
config.ollabridge = {
base_url: settings.ollabridge.base_url,
model: settings.ollabridge.model,
api_key: settings.ollabridge.api_key,
};
}
const result = await testProvider(config);
setTestResult(result);
} catch (err) {
setTestResult({
health: "error",
warning: err.message || "Test failed",
});
} finally {
setTesting(false);
}
};
if (initialLoading) {
return (
<LoadingState
loadingMessage={loadingMessage}
loadingSlow={loadingSlow}
onRetry={loadSettings}
/>
);
}
if (!settings) {
return (
<div className="settings-root">
<div className="settings-inline-error-card">
<h1>AI Providers</h1>
<div className="settings-loading-subtitle">Admin / LLM Settings</div>
<p className="settings-error-text">
{error || "Unable to load current configuration."}
</p>
<button
type="button"
className="settings-secondary-btn"
onClick={loadSettings}
>
Retry
</button>
</div>
</div>
);
}
const { provider } = settings;
const availableModels = modelsByProvider[provider] || [];
return (
<div className="settings-root">
<h1>AI Providers</h1>
<p className="settings-muted">
Choose which LLM provider GitPilot should use for planning and agent
workflows. Provider settings are stored on the server.
</p>
{error && <div className="settings-error-banner">{error}</div>}
{savedMsg && <div className="settings-success-banner">{savedMsg}</div>}
<div className="settings-card">
<label className="settings-label">Active provider</label>
<div className="settings-provider-tabs">
{PROVIDERS.map((p) => (
<button
key={p}
type="button"
className={
"settings-provider-tab" +
(provider === p ? " settings-provider-tab-active" : "")
}
onClick={() => setSettings((prev) => ({ ...prev, provider: p }))}
>
{PROVIDER_LABELS[p] || p}
</button>
))}
</div>
</div>
{provider === "ollabridge" && (
<div className="settings-card">
<div className="settings-title">OllaBridge Cloud Configuration</div>
<div className="settings-hint" style={{ marginBottom: 12 }}>
Connect to OllaBridge Cloud or any OllaBridge instance for LLM
inference. No API key required for public endpoints.
</div>
<label className="settings-label">Authentication Mode</label>
<div className="ob-auth-tabs">
{AUTH_MODES.map((m) => (
<button
key={m.id}
type="button"
className={
"ob-auth-tab" +
(authMode === m.id ? " ob-auth-tab-active" : "")
}
onClick={() => setAuthMode(m.id)}
>
<span className="ob-auth-tab-icon">{m.icon}</span>
<span>{m.label}</span>
</button>
))}
</div>
{authMode === "device" && (
<div className="ob-auth-panel">
<div className="ob-auth-desc">
Enter the pairing code from your OllaBridge console and click
Pair.
</div>
<div className="ob-pair-row">
<input
className="settings-input ob-pair-input"
type="text"
maxLength={9}
placeholder="ABCD-1234"
value={pairCode}
onChange={(e) => setPairCode(e.target.value.toUpperCase())}
onKeyDown={(e) => e.key === "Enter" && handlePair()}
/>
<button
type="button"
className="ob-pair-btn"
onClick={handlePair}
disabled={pairing || !pairCode.trim()}
>
{pairing ? "Pairing…" : "Pair"}
</button>
</div>
{pairResult && (
<div
className={
pairResult.ok ? "settings-success-banner" : "settings-error-banner"
}
>
{pairResult.message}
</div>
)}
</div>
)}
<label className="settings-label">Base URL</label>
<input
className="settings-input"
value={settings.ollabridge?.base_url || ""}
onChange={(e) =>
updateField("ollabridge", "base_url", e.target.value)
}
placeholder="https://your-ollabridge-endpoint"
/>
{(authMode === "apikey" || authMode === "local") && (
<>
<label className="settings-label">API Key</label>
<input
className="settings-input"
type="password"
value={settings.ollabridge?.api_key || ""}
onChange={(e) =>
updateField("ollabridge", "api_key", e.target.value)
}
placeholder="Optional API key"
/>
</>
)}
<label className="settings-label">Model</label>
<div className="settings-inline-row">
<input
className="settings-input"
value={settings.ollabridge?.model || ""}
onChange={(e) =>
updateField("ollabridge", "model", e.target.value)
}
placeholder="qwen2.5:1.5b"
/>
<button
type="button"
className="settings-secondary-btn"
onClick={() => loadModelsForProvider("ollabridge")}
disabled={loadingModelsFor === "ollabridge"}
>
{loadingModelsFor === "ollabridge" ? "Loading…" : "Load Models"}
</button>
</div>
</div>
)}
{provider === "openai" && (
<div className="settings-card">
<div className="settings-title">OpenAI Configuration</div>
<label className="settings-label">API Key</label>
<input
className="settings-input"
type="password"
value={settings.openai?.api_key || ""}
onChange={(e) => updateField("openai", "api_key", e.target.value)}
placeholder="sk-..."
/>
<label className="settings-label">Base URL</label>
<input
className="settings-input"
value={settings.openai?.base_url || ""}
onChange={(e) => updateField("openai", "base_url", e.target.value)}
placeholder="Optional custom base URL"
/>
<label className="settings-label">Model</label>
<input
className="settings-input"
value={settings.openai?.model || ""}
onChange={(e) => updateField("openai", "model", e.target.value)}
placeholder="gpt-4o-mini"
/>
</div>
)}
{provider === "claude" && (
<div className="settings-card">
<div className="settings-title">Claude Configuration</div>
<label className="settings-label">API Key</label>
<input
className="settings-input"
type="password"
value={settings.claude?.api_key || ""}
onChange={(e) => updateField("claude", "api_key", e.target.value)}
placeholder="Anthropic API key"
/>
<label className="settings-label">Base URL</label>
<input
className="settings-input"
value={settings.claude?.base_url || ""}
onChange={(e) => updateField("claude", "base_url", e.target.value)}
placeholder="Optional custom base URL"
/>
<label className="settings-label">Model</label>
<input
className="settings-input"
value={settings.claude?.model || ""}
onChange={(e) => updateField("claude", "model", e.target.value)}
placeholder="claude-sonnet-4-5"
/>
</div>
)}
{provider === "watsonx" && (
<div className="settings-card">
<div className="settings-title">Watsonx Configuration</div>
<label className="settings-label">API Key</label>
<input
className="settings-input"
type="password"
value={settings.watsonx?.api_key || ""}
onChange={(e) => updateField("watsonx", "api_key", e.target.value)}
placeholder="Watsonx API key"
/>
<label className="settings-label">Project ID</label>
<input
className="settings-input"
value={settings.watsonx?.project_id || ""}
onChange={(e) =>
updateField("watsonx", "project_id", e.target.value)
}
placeholder="Watsonx project ID"
/>
<label className="settings-label">Base URL</label>
<input
className="settings-input"
value={settings.watsonx?.base_url || ""}
onChange={(e) => updateField("watsonx", "base_url", e.target.value)}
placeholder="https://api.watsonx.ai/v1"
/>
<label className="settings-label">Model</label>
<input
className="settings-input"
value={settings.watsonx?.model_id || ""}
onChange={(e) =>
updateField("watsonx", "model_id", e.target.value)
}
placeholder="meta-llama/llama-3-3-70b-instruct"
/>
</div>
)}
{provider === "ollama" && (
<div className="settings-card">
<div className="settings-title">Ollama Configuration</div>
<label className="settings-label">Base URL</label>
<input
className="settings-input"
value={settings.ollama?.base_url || ""}
onChange={(e) => updateField("ollama", "base_url", e.target.value)}
placeholder="http://localhost:11434"
/>
<label className="settings-label">Model</label>
<div className="settings-inline-row">
<input
className="settings-input"
value={settings.ollama?.model || ""}
onChange={(e) => updateField("ollama", "model", e.target.value)}
placeholder="llama3"
/>
<button
type="button"
className="settings-secondary-btn"
onClick={() => loadModelsForProvider("ollama")}
disabled={loadingModelsFor === "ollama"}
>
{loadingModelsFor === "ollama" ? "Loading…" : "Load Models"}
</button>
</div>
</div>
)}
{availableModels.length > 0 && (
<div className="settings-card">
<div className="settings-title">Available Models</div>
<div className="settings-model-list">
{availableModels.map((model) => (
<button
key={model}
type="button"
className="settings-model-chip"
onClick={() => updateField(provider, "model", model)}
>
{model}
</button>
))}
</div>
</div>
)}
{modelsError && <div className="settings-error-banner">{modelsError}</div>}
{testResult && (
<div
className={
testResult.health === "ok"
? "settings-success-banner"
: "settings-error-banner"
}
>
{testResult.health === "ok"
? testResult.details || "Provider connection successful."
: testResult.warning || "Provider connection failed."}
</div>
)}
<div className="settings-actions">
<button
type="button"
className="settings-save-btn"
onClick={handleSave}
disabled={saving}
>
{saving ? "Saving…" : "Save Settings"}
</button>
<button
type="button"
className="settings-secondary-btn"
onClick={handleTestConnection}
disabled={testing}
>
{testing ? "Testing…" : "Test Connection"}
</button>
</div>
</div>
);
}