minor UI fixes
This commit is contained in:
@@ -19,6 +19,8 @@ export function SettingsPanel() {
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [conn, setConn] = useState<{ ok: boolean; models: string[]; error: string } | null>(null);
|
||||
const { addToast } = useToast();
|
||||
|
||||
// ponytail: auto-load on mount so users don't see a gate before the form.
|
||||
@@ -47,6 +49,59 @@ export function SettingsPanel() {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
// ponytail: probe the configured endpoint for a model list. Ollama uses
|
||||
// /api/tags, OpenAI-compatible uses /v1/models. The Rust side branches.
|
||||
async function testConnection() {
|
||||
if (!config) return;
|
||||
setTesting(true);
|
||||
setConn(null);
|
||||
try {
|
||||
// Save first so the backend uses the latest URL/key.
|
||||
await invoke("set_llm_config", { config });
|
||||
const result = await invoke<{ ok: boolean; models: string[]; error: string }>("test_connection");
|
||||
setConn(result);
|
||||
if (result.ok) addToast(`Connected — ${result.models.length} models`, "success");
|
||||
else addToast(`Connection failed: ${result.error}`, "error");
|
||||
} catch (e) {
|
||||
setConn({ ok: false, models: [], error: String(e) });
|
||||
addToast(`Connection failed: ${e}`, "error");
|
||||
}
|
||||
setTesting(false);
|
||||
}
|
||||
|
||||
// ponytail: provider presets fill in the API URL pattern + default model.
|
||||
const PRESETS: { label: string; url: string; model: string; key?: boolean }[] = [
|
||||
{ label: "Ollama", url: "http://localhost:11434", model: "llama3.2" },
|
||||
{ label: "LM Studio", url: "http://localhost:1234/v1", model: "local-model" },
|
||||
{ label: "OpenAI", url: "https://api.openai.com", model: "gpt-4o-mini", key: true },
|
||||
{ label: "Custom", url: "", model: "" },
|
||||
];
|
||||
function applyPreset(p: { url: string; model: string }) {
|
||||
if (!config) return;
|
||||
setConfig({ ...config, api_url: p.url, model: p.model });
|
||||
setConn(null);
|
||||
}
|
||||
|
||||
// ponytail: reset to the backend's default config. The Rust Default impl
|
||||
// is the single source of truth; we just set an empty config and let the
|
||||
// backend re-default on save — but we can't send a partial, so mirror the
|
||||
// known defaults here and toast.
|
||||
const DEFAULTS: LlmConfig = {
|
||||
api_url: "http://localhost:11434",
|
||||
api_key: "",
|
||||
model: "llama3.2",
|
||||
temperature: 0.7,
|
||||
max_tokens: 512,
|
||||
top_p: 0.9,
|
||||
image_model: "x/flux2-klein:4b",
|
||||
embed_model: "nomic-embed-text",
|
||||
};
|
||||
function resetDefaults() {
|
||||
setConfig({ ...DEFAULTS });
|
||||
setConn(null);
|
||||
addToast("Reset to defaults — click Save to apply", "info");
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-3">
|
||||
@@ -60,6 +115,28 @@ export function SettingsPanel() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 text-sm">
|
||||
<h3 className="font-heading text-[var(--color-gold-bright)] text-xs font-semibold uppercase tracking-wider border-b border-[var(--color-border-subtle)] pb-1 mb-1">LLM Connection</h3>
|
||||
{/* Provider presets */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[var(--color-text-secondary)] text-xs font-medium">Provider</label>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{PRESETS.map((p) => (
|
||||
<button
|
||||
key={p.label}
|
||||
type="button"
|
||||
onClick={() => applyPreset(p)}
|
||||
className={`rounded-lg px-2.5 py-1 text-xs border transition-colors cursor-pointer ${
|
||||
config.api_url === p.url
|
||||
? "bg-[var(--color-gold-glow)] border-[var(--color-gold-bright)] text-[var(--color-gold-bright)]"
|
||||
: "bg-[var(--color-bg-surface)] border-[var(--color-border-glass)] text-[var(--color-text-dim)] hover:text-[var(--color-text-secondary)]"
|
||||
}`}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
|
||||
API URL
|
||||
@@ -104,9 +181,51 @@ export function SettingsPanel() {
|
||||
value={config.model}
|
||||
onChange={(e) => setConfig({ ...config, model: e.target.value })}
|
||||
placeholder="llama3.2"
|
||||
list="model-list"
|
||||
/>
|
||||
<datalist id="model-list">
|
||||
{conn?.models.map((m) => (
|
||||
<option key={m} value={m} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
{/* Connection test */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={testConnection}
|
||||
disabled={testing}
|
||||
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1.5 text-xs font-medium text-[var(--color-text-secondary)] hover:border-[var(--color-gold-bright)] hover:text-[var(--color-gold-bright)] transition-colors cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{testing ? "Testing…" : "Test connection"}
|
||||
</button>
|
||||
{conn && (
|
||||
<span className={`text-xs ${conn.ok ? "text-[var(--color-success)]" : "text-[var(--color-danger)]"}`}>
|
||||
{conn.ok ? `✓ ${conn.models.length} models` : `✕ ${conn.error}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{conn?.ok && conn.models.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 max-h-24 overflow-y-auto">
|
||||
{conn.models.map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => setConfig({ ...config, model: m })}
|
||||
className={`rounded-full px-2 py-0.5 text-[10px] border cursor-pointer transition-colors ${
|
||||
config.model === m
|
||||
? "bg-[var(--color-gold-glow)] border-[var(--color-gold-bright)] text-[var(--color-gold-bright)]"
|
||||
: "bg-[var(--color-bg-deep)] border-[var(--color-border-subtle)] text-[var(--color-text-dim)] hover:text-[var(--color-text-secondary)]"
|
||||
}`}
|
||||
>
|
||||
{m}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 className="font-heading text-[var(--color-gold-bright)] text-xs font-semibold uppercase tracking-wider border-b border-[var(--color-border-subtle)] pb-1 mb-1">Image Model</h3>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
|
||||
Image model
|
||||
@@ -119,6 +238,7 @@ export function SettingsPanel() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="font-heading text-[var(--color-gold-bright)] text-xs font-semibold uppercase tracking-wider border-b border-[var(--color-border-subtle)] pb-1 mb-1">Embedding Model (Lore RAG)</h3>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
|
||||
Embedding model (Lore RAG)
|
||||
@@ -131,6 +251,7 @@ export function SettingsPanel() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="font-heading text-[var(--color-gold-bright)] text-xs font-semibold uppercase tracking-wider border-b border-[var(--color-border-subtle)] pb-1 mb-1">Text Model</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
|
||||
@@ -166,13 +287,21 @@ export function SettingsPanel() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={saveConfig}
|
||||
disabled={loading}
|
||||
className="rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-4 py-2 text-sm font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{saved ? "✓ Saved" : loading ? "Saving…" : "Save Settings"}
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={saveConfig}
|
||||
disabled={loading}
|
||||
className="rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-4 py-2 text-sm font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{saved ? "✓ Saved" : loading ? "Saving…" : "Save Settings"}
|
||||
</button>
|
||||
<button
|
||||
onClick={resetDefaults}
|
||||
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] text-[var(--color-text-dim)] px-4 py-2 text-sm hover:text-[var(--color-danger)] hover:border-[var(--color-danger)] transition-colors cursor-pointer"
|
||||
>
|
||||
Reset to defaults
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user