feat: add SettingsPage with HuggingFace model search and fix flexbox layout

- Add SettingsPage component with macOS/Linear-style settings UI (sp-* classes)
- Fix HuggingFace results list squeeze: sp-card-row--full now has align-items: stretch
  overriding the inherited flex-start, ensuring children fill 100% horizontal width
- Fix sp-hf-file row layout: width 100%, grid columns for name/quant/size/download
- Fix sp-hf-name truncation: explicit overflow/ellipsis/white-space in CSS
- Add white-space: nowrap to sp-hf-size and sp-hf-quant-badge to protect secondary columns
- Add runtime-deps crate, Tailwind/PostCSS config, runtime-defaults, MessageBubble test
- Update Cargo workspace, inference-server, tauri runtime types and process handling
This commit is contained in:
Evandro Fonseca Junior committed 2026-03-30 23:35:08 -03:00
1 parent 9da25c3688
commit 01b629cb4a
41 files changed
+6864 -822

No files matched your search

@@ -0,0 +1,922 @@
import { useCallback, useEffect, useRef, useState } from "react";
import {
pickRuntimeModelFiles,
pickRuntimeModelsDir,
queryRuntimeGpu,
searchHuggingFaceModels,
} from "../runtime/api";
import {
dedupeModels,
pathName,
toModelFile,
} from "../runtime/runtime-settings-form";
import { CTX_PRESETS, DEFAULT_CTX_SIZE, MIN_CTX_SIZE } from "../runtime/runtime-defaults";
import { getRuntimeStateLabel, getRuntimeStatusTone } from "../runtime/runtime-utils";
import type {
HuggingFaceModelResult,
RuntimeConfig,
RuntimeGpuInfo,
RuntimeModelFile,
RuntimeSnapshot,
} from "../runtime/types";
import { Box, Zap, ArrowLeftRight, Settings2, X, Trash2, Search, Info, Download } from "lucide-react";
import "./SettingsPage.css";
// ─── Types ───────────────────────────────────────────────────────────────────
type Section = "model" | "runtime" | "connection" | "advanced";
interface SettingsRuntime {
snapshot: RuntimeSnapshot;
savedBaseline: RuntimeConfig;
draftConfig: RuntimeConfig;
draftDirty: boolean;
loading: boolean;
busy: boolean;
lifecycleBusy: boolean;
scannedModels: RuntimeModelFile[];
isScanningModels: boolean;
updateDraft: (patch: Partial<RuntimeConfig>) => void;
saveConfig: (patch: Partial<RuntimeConfig>) => Promise<RuntimeSnapshot>;
startLocal: () => Promise<unknown>;
stopLocal: () => Promise<unknown>;
scanModels: (dir: string | null) => Promise<void>;
resetDraft: () => void;
}
interface Props {
runtime: SettingsRuntime;
onClose: () => void;
}
// ─── Nav definition ──────────────────────────────────────────────────────────
const NAV: { id: Section; label: string; icon: typeof Box }[] = [
{ id: "model", label: "Model", icon: Box },
{ id: "runtime", label: "Runtime", icon: Zap },
{ id: "connection", label: "Connection", icon: ArrowLeftRight },
{ id: "advanced", label: "Advanced", icon: Settings2 },
];
// ─── Helpers ─────────────────────────────────────────────────────────────────
function formatBytes(bytes: number | null | undefined): string {
if (!bytes) return "";
if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GiB`;
if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(0)} MiB`;
return `${(bytes / 1024).toFixed(0)} KiB`;
}
function statusColor(snapshot: RuntimeSnapshot): string {
const tone = getRuntimeStatusTone(snapshot);
if (tone === "success") return "sp-dot--success";
if (tone === "warning") return "sp-dot--warn";
if (tone === "danger") return "sp-dot--danger";
return "sp-dot--idle";
}
function statusPillClass(snapshot: RuntimeSnapshot): string {
const tone = getRuntimeStatusTone(snapshot);
if (tone === "success") return "sp-status-pill--success";
if (tone === "warning") return "sp-status-pill--warn";
if (tone === "danger") return "sp-status-pill--danger";
return "";
}
// ─── SettingsPage ─────────────────────────────────────────────────────────────
export default function SettingsPage({ runtime, onClose }: Props) {
const {
snapshot, savedBaseline, draftConfig, draftDirty,
loading, busy, lifecycleBusy,
scannedModels, isScanningModels,
updateDraft, saveConfig, startLocal, stopLocal, scanModels, resetDraft,
} = runtime;
const [section, setSection] = useState<Section>("model");
const [hfQuery, setHfQuery] = useState("");
const [hfResults, setHfResults] = useState<HuggingFaceModelResult[]>([]);
const [hfLoading, setHfLoading] = useState(false);
const [gpuInfo, setGpuInfo] = useState<RuntimeGpuInfo | null>(null);
const [localError, setLocalError] = useState("");
const [saving, setSaving] = useState(false);
const didAutoScan = useRef(false);
const busyAll = busy || lifecycleBusy || saving;
// ── Side-effects ──────────────────────────────────────────────────────────
useEffect(() => {
queryRuntimeGpu().then(setGpuInfo).catch(() => undefined);
}, []);
useEffect(() => {
if (section !== "model" || didAutoScan.current) return;
const dir = draftConfig.models_dir?.trim();
if (dir && scannedModels.length === 0 && !isScanningModels) {
didAutoScan.current = true;
void scanModels(dir);
}
}, [section, draftConfig.models_dir, scannedModels.length, isScanningModels, scanModels]);
// ── Derived data ──────────────────────────────────────────────────────────
const allModels = dedupeModels([
...scannedModels,
...draftConfig.extra_model_paths.map(toModelFile),
]);
const isLocal = draftConfig.connection_mode === "local";
const isRunning = snapshot.process_running;
const isStopping = snapshot.status === "stopping";
const runtimeStatusLabel = loading ? "Loading..." : getRuntimeStateLabel(snapshot);
// ── Handlers ─────────────────────────────────────────────────────────────
const handlePickDir = useCallback(async () => {
setLocalError("");
try {
const result = await pickRuntimeModelsDir();
if (!result) return;
updateDraft({ models_dir: result.dir });
void scanModels(result.dir);
} catch (e) {
setLocalError(String(e));
}
}, [updateDraft, scanModels]);
const handlePickFiles = useCallback(async () => {
setLocalError("");
try {
const picked = await pickRuntimeModelFiles();
if (picked.length === 0) return;
const next = dedupeModels([
...draftConfig.extra_model_paths.map(toModelFile),
...picked,
]);
updateDraft({
extra_model_paths: next.map((m) => m.path),
model_path: draftConfig.model_path || next[0]?.path || null,
});
} catch (e) {
setLocalError(String(e));
}
}, [draftConfig, updateDraft]);
const handleSelectModel = useCallback((path: string) => {
updateDraft({ model_path: path, selected_model_id: null });
}, [updateDraft]);
const handleRemoveExtra = useCallback((path: string) => {
const next = draftConfig.extra_model_paths.filter((p) => p !== path);
updateDraft({
extra_model_paths: next,
model_path: draftConfig.model_path === path
? (next[0] ?? scannedModels[0]?.path ?? null)
: draftConfig.model_path,
});
}, [draftConfig, scannedModels, updateDraft]);
const handleHfSearch = useCallback(async () => {
if (!hfQuery.trim()) return;
setHfLoading(true);
setLocalError("");
try {
const results = await searchHuggingFaceModels(hfQuery, draftConfig.hugging_face_token || "");
setHfResults(results);
} catch (e) {
setLocalError(String(e));
} finally {
setHfLoading(false);
}
}, [hfQuery, draftConfig.hugging_face_token]);
const handleSave = useCallback(async () => {
setSaving(true);
setLocalError("");
try {
await saveConfig(draftConfig);
} catch (e) {
setLocalError(String(e));
} finally {
setSaving(false);
}
}, [saveConfig, draftConfig]);
const handleSaveAndStart = useCallback(async () => {
setSaving(true);
setLocalError("");
try {
await saveConfig(draftConfig);
await startLocal();
} catch (e) {
setLocalError(String(e));
} finally {
setSaving(false);
}
}, [saveConfig, draftConfig, startLocal]);
const handleStop = useCallback(async () => {
setLocalError("");
try {
await stopLocal();
} catch (e) {
setLocalError(String(e));
}
}, [stopLocal]);
// ── Render ───────────────────────────────────────────────────────────────
return (
<div className="sp-shell flex h-full overflow-hidden">
{/* ── Left navigation ──────────────────────────────────────────── */}
<nav className="sp-nav">
<div className="sp-nav-header">
<div className="sp-nav-brand">ForgeLM</div>
<h2 className="sp-nav-title">Settings</h2>
</div>
<div className="sp-nav-items">
{NAV.map((item) => {
const Icon = item.icon;
return (
<button
key={item.id}
type="button"
onClick={() => setSection(item.id)}
className={`sp-nav-item${section === item.id ? " sp-nav-item--active" : ""}`}
>
<Icon size={16} strokeWidth={1.5} />
<span className="flex-1">{item.label}</span>
</button>
);
})}
</div>
{/* Runtime status pill */}
<div className="sp-nav-status">
<span className={`sp-dot ${statusColor(snapshot)}`} />
<span className="text-xs sp-nav-status-label">
{runtimeStatusLabel}
</span>
</div>
<div className="sp-nav-footer">
<button type="button" onClick={onClose} className="sp-back-btn">
← Back to Chat
</button>
</div>
</nav>
{/* ── Main area (right side) ─────────────────────────────────── */}
<div className="settings-main-area">
<div className="settings-scroll-container">
<div className="settings-content">
{/* ═══════════════════════════════════ MODEL SECTION ═══════════════════════════ */}
{section === "model" && (
<>
<header className="page-header sp-section-header">
<div>
<h2 className="sp-section-title">Model</h2>
<p className="sp-section-desc">
Select a local GGUF model file for inference.
</p>
</div>
{isRunning && (
<span className={`sp-status-pill ${statusPillClass(snapshot)}`}>
<span className="sp-dot sp-dot--inline" />
{isStopping ? "Stopping local API" : getRuntimeStateLabel(snapshot)}
</span>
)}
</header>
<div className="settings-cards">
{localError && <div className="sp-error-inline">{localError}</div>}
{/* Active model */}
<div className="sp-card">
{snapshot.config.model_path ? (
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">Active model</div>
<div className="sp-card-row-desc">Currently loaded GGUF file</div>
</div>
<div className="sp-card-row-control">
<div className="sp-model-name">{pathName(snapshot.config.model_path)}</div>
<div className="sp-model-path">{snapshot.config.model_path}</div>
{draftDirty && draftConfig.model_path !== snapshot.config.model_path && (
<div className="sp-pending mt-2">
Draft → {pathName(draftConfig.model_path) || "None selected"}
</div>
)}
</div>
</div>
) : (
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">Active model</div>
<div className="sp-card-row-desc">No model selected yet</div>
</div>
<div className="sp-card-row-control">
<p className="sp-empty">Use the sections below to load one.</p>
</div>
</div>
)}
{/* Scan folder */}
<div className="sp-card-divider" />
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">Scan folder</div>
<div className="sp-card-row-desc">Browse and scan a directory for .gguf files</div>
</div>
<div className="sp-card-row-control">
<div className="sp-folder-actions">
<button
type="button"
className="sp-btn sp-btn--secondary"
onClick={() => void handlePickDir()}
disabled={busyAll}
>
Browse…
</button>
{draftConfig.models_dir && (
<button
type="button"
className="sp-btn sp-btn--ghost"
onClick={() => { void scanModels(draftConfig.models_dir!); }}
disabled={busyAll || isScanningModels}
>
{isScanningModels ? "Scanning…" : "Rescan"}
</button>
)}
</div>
{draftConfig.models_dir && (
<div className="sp-folder-path">{draftConfig.models_dir}</div>
)}
</div>
</div>
{/* Model list from scan */}
{(isScanningModels || scannedModels.length > 0) && (
<>
<div className="sp-card-divider" />
<div className="sp-card-row sp-card-row--full">
{isScanningModels && scannedModels.length === 0 && (
<div className="sp-scanning-row">Scanning for models…</div>
)}
<div className="sp-model-list">
{scannedModels.map((model) => {
const selected = draftConfig.model_path === model.path;
return (
<button
key={model.path}
type="button"
className={`sp-model-row${selected ? " sp-model-row--selected" : ""}`}
onClick={() => handleSelectModel(model.path)}
disabled={busyAll}
>
<span className={`sp-radio${selected ? " sp-radio--on" : ""}`} />
<span className="flex-1 truncate sp-model-row-name">{model.name}</span>
</button>
);
})}
</div>
</div>
</>
)}
{/* Browse individual files */}
<div className="sp-card-divider" />
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">Individual files</div>
<div className="sp-card-row-desc">Add specific .gguf files without scanning a folder</div>
</div>
<div className="sp-card-row-control">
<button
type="button"
className="sp-btn sp-btn--secondary"
onClick={() => void handlePickFiles()}
disabled={busyAll}
>
Pick files…
</button>
{draftConfig.extra_model_paths.length > 0 && (
<div className="sp-model-list mt-3">
{draftConfig.extra_model_paths.map((p) => {
const selected = draftConfig.model_path === p;
return (
<div
key={p}
className={`sp-model-row${selected ? " sp-model-row--selected" : ""}${busyAll ? " sp-model-row--disabled" : ""}`}
role="row"
>
<button
type="button"
className="sp-model-row-select"
onClick={() => handleSelectModel(p)}
disabled={busyAll}
aria-pressed={selected}
>
<span className={`sp-radio${selected ? " sp-radio--on" : ""}`} />
<span className="flex-1 truncate sp-model-row-name">{pathName(p)}</span>
</button>
<button
type="button"
className="sp-remove-btn"
onClick={() => handleRemoveExtra(p)}
disabled={busyAll}
aria-label="Remove file"
>
<Trash2 size={14} strokeWidth={1.5} />
</button>
</div>
);
})}
</div>
)}
</div>
</div>
{/* HuggingFace search */}
<div className="sp-card-divider" />
<div className="sp-card-row sp-card-row--full">
<div className="sp-hf-search-header">
<div>
<div className="sp-card-row-title">Search HuggingFace</div>
<div className="sp-card-row-desc">
{!draftConfig.hugging_face_token && "Add HF token in Advanced for private models"}
</div>
</div>
<span className="tooltip-container">
<Info size={14} className="sp-hf-info-icon" />
<div className="sp-tooltip-content">
<div className="sp-hf-tooltip-title">O que é Quantização?</div>
<p className="sp-hf-tooltip-body">
É a compressão do modelo para que ele rode mais rápido e caiba na memória (RAM/VRAM) do seu PC.
</p>
<div className="sp-hf-tooltip-levels">
<span><span className="sp-hf-dot sp-hf-dot--green" /> <strong>Máxima Qualidade (Pesado):</strong> Q8_0, Q6_K — Quase sem perda de inteligência.</span>
<span><span className="sp-hf-dot sp-hf-dot--blue" /> <strong>Equilíbrio Ideal (Recomendado):</strong> Q4_K_M, Q5_K_M — Melhor custo-benefício entre velocidade e resposta.</span>
<span><span className="sp-hf-dot sp-hf-dot--yellow" /> <strong>Alta Compressão (Leve):</strong> Q2_K, Q3_K_M — Roda em PCs fracos, mas o modelo fica mais "esquecido" ou impreciso.</span>
</div>
</div>
</span>
</div>
<div className="sp-hf-search-bar">
<input
className="sp-input sp-hf-input"
placeholder="Search for GGUF models…"
value={hfQuery}
onChange={(e) => setHfQuery(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") void handleHfSearch(); }}
/>
<button
type="button"
className="sp-btn sp-btn--primary"
onClick={() => void handleHfSearch()}
disabled={hfLoading || !hfQuery.trim()}
>
<Search size={14} strokeWidth={2} />
{hfLoading ? "…" : "Search"}
</button>
</div>
{hfResults.length > 0 && (
<div className="sp-hf-results">
{hfResults.map((result) => (
<div key={result.repo_id} className="sp-hf-card">
<div className="sp-hf-card-header">
<div className="sp-hf-repo">{result.repo_id}</div>
<div className="sp-hf-author">{result.author || "Unknown author"}</div>
</div>
{result.verified_files.length > 0 && (
<div className="sp-hf-files">
{result.verified_files.map((file) => {
const fileName = file.path.replace(/\.gguf$/i, "");
const fileSize = file.size ? `${(file.size / 1_073_741_824).toFixed(1)} GB` : "—";
return (
<div key={`${result.repo_id}:${file.path}`} className="sp-hf-file">
<span className="sp-hf-name truncate" title={file.path}>{fileName}</span>
<span className="sp-hf-size">{fileSize}</span>
<span className="sp-hf-quant-badge">{file.quantization || "gguf"}</span>
<button
type="button"
className="sp-hf-download-btn"
title={`Download ${file.path}`}
onClick={() => {
// TODO: wire up actual download
console.log("Download:", file.path);
}}
>
<Download size={16} strokeWidth={2} />
</button>
</div>
);
})}
</div>
)}
</div>
))}
</div>
)}
{!hfLoading && hfQuery && hfResults.length === 0 && (
<p className="sp-hint mt-3">No results. Try a different query.</p>
)}
</div>
</div>
</div>
</>
)}
{/* ═══════════════════════════════════ RUNTIME SECTION ════════════════════════ */}
{section === "runtime" && (
<>
<header className="page-header sp-section-header">
<div>
<h2 className="sp-section-title">Runtime</h2>
<p className="sp-section-desc">GPU acceleration, context window, and startup policy.</p>
</div>
</header>
<div className="settings-cards">
{localError && <div className="sp-error-inline">{localError}</div>}
<div className="sp-card">
{/* GPU acceleration */}
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">GPU acceleration</div>
<div className="sp-card-row-desc">
{gpuInfo ? `${gpuInfo.name} · ${gpuInfo.vram_mib} MiB VRAM` : "No GPU detected — CPU only"}
</div>
</div>
<div className="sp-card-row-control">
{gpuInfo && (
<span className="sp-status-pill sp-status-pill--success">GPU detected</span>
)}
</div>
</div>
<div className="sp-card-divider" />
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">GPU layers</div>
<div className="sp-card-row-desc">Layers to offload to the GPU</div>
</div>
<div className="sp-card-row-control sp-card-row-control--wide">
<div className="sp-field-value">{draftConfig.gpu_layers}</div>
<input
type="range"
min={0}
max={gpuInfo ? Math.max(gpuInfo.recommended_layers, draftConfig.gpu_layers, 64) : 64}
value={draftConfig.gpu_layers}
onChange={(e) => updateDraft({ gpu_layers: Number(e.target.value) })}
className="sp-slider w-full mt-2"
disabled={busyAll}
/>
<div className="sp-slider-presets">
<button
type="button"
className="sp-btn-xs"
onClick={() => updateDraft({ gpu_layers: 0 })}
>
0 — CPU only
</button>
{gpuInfo && (
<button
type="button"
className="sp-btn-xs"
onClick={() => updateDraft({ gpu_layers: gpuInfo.recommended_layers })}
>
{gpuInfo.recommended_layers} — Recommended
</button>
)}
</div>
</div>
</div>
{/* Context window */}
<div className="sp-card-divider" />
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">Context window</div>
<div className="sp-card-row-desc">Maximum tokens the model can process</div>
</div>
<div className="sp-card-row-control sp-card-row-control--wide">
<div className="sp-field-value">{draftConfig.ctx_size.toLocaleString()} tokens</div>
<div className="sp-segmented">
{CTX_PRESETS.map((preset) => (
<button
key={preset}
type="button"
onClick={() => updateDraft({ ctx_size: preset })}
className={`sp-segmented-btn${draftConfig.ctx_size === preset ? " sp-segmented-btn--active" : ""}`}
disabled={busyAll}
>
{preset >= 1024 ? `${preset / 1024}K` : preset}
</button>
))}
</div>
<div className="sp-ctx-custom">
<input
type="number"
className="sp-input sp-input--sm"
value={draftConfig.ctx_size}
min={MIN_CTX_SIZE}
onChange={(e) => {
const v = Number(e.target.value);
if (v >= MIN_CTX_SIZE) updateDraft({ ctx_size: v });
}}
disabled={busyAll}
/>
<span className="sp-hint">Min {MIN_CTX_SIZE.toLocaleString()}</span>
</div>
</div>
</div>
{/* Startup policy */}
<div className="sp-card-divider" />
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">Startup policy</div>
<div className="sp-card-row-desc">When to start the local API</div>
</div>
<div className="sp-card-row-control">
<div className="sp-toggles">
{(["manual", "on_app_launch"] as const).map((policy) => {
const active = draftConfig.start_policy === policy;
return (
<label key={policy} className={`sp-toggle${active ? " sp-toggle--active" : ""}`}>
<input
type="checkbox"
checked={active}
onChange={() => updateDraft({ start_policy: policy })}
disabled={busyAll}
/>
<span className="sp-toggle-track" />
<span className="sp-toggle-label">
{policy === "manual" ? "Manual" : "Auto-start"}
</span>
</label>
);
})}
</div>
<div className="sp-toggle-desc">
{draftConfig.start_policy === "manual"
? "Start the local API from the runtime panel when needed."
: "Automatically start the local API when ForgeLM opens."}
</div>
</div>
</div>
</div>
</div>
</>
)}
{/* ═══════════════════════════════════ CONNECTION SECTION ═════════════════════ */}
{section === "connection" && (
<>
<header className="page-header sp-section-header">
<div>
<h2 className="sp-section-title">Connection</h2>
<p className="sp-section-desc">
Use the built-in local inference engine or connect to a remote OpenAI-compatible server.
</p>
</div>
</header>
<div className="settings-cards">
{localError && <div className="sp-error-inline">{localError}</div>}
<div className="sp-card">
{/* Mode */}
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">Mode</div>
<div className="sp-card-row-desc">
{draftConfig.connection_mode === "local"
? "Runs the inference server locally on your machine."
: "Forwards all requests to an external OpenAI-compatible endpoint."}
</div>
</div>
<div className="sp-card-row-control">
<div className="sp-segmented">
{(["local", "remote"] as const).map((mode) => {
const active = draftConfig.connection_mode === mode;
return (
<button
key={mode}
type="button"
className={`sp-segmented-btn${active ? " sp-segmented-btn--active" : ""}`}
onClick={() => updateDraft({ connection_mode: mode })}
disabled={busyAll}
>
{mode === "local" ? "Local API" : "Remote API"}
</button>
);
})}
</div>
</div>
</div>
{/* Local settings */}
{draftConfig.connection_mode === "local" && (
<>
<div className="sp-card-divider" />
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">Bind address</div>
<div className="sp-card-row-desc">Host and port for the local API</div>
</div>
<div className="sp-card-row-control">
<input
className="sp-input w-full"
value={draftConfig.addr}
onChange={(e) => updateDraft({ addr: e.target.value })}
placeholder="127.0.0.1:8080"
disabled={busyAll}
/>
<p className="sp-hint mt-2">
Available at{" "}
<code>http://{draftConfig.addr || "127.0.0.1:8080"}</code>
</p>
</div>
</div>
</>
)}
{/* Remote settings */}
{draftConfig.connection_mode === "remote" && (
<>
<div className="sp-card-divider" />
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">Server URL</div>
<div className="sp-card-row-desc">Base URL of the remote API</div>
</div>
<div className="sp-card-row-control">
<input
className="sp-input w-full"
value={draftConfig.server_url}
onChange={(e) => updateDraft({ server_url: e.target.value })}
placeholder="https://api.openai.com/v1"
disabled={busyAll}
/>
</div>
</div>
<div className="sp-card-divider" />
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">API key</div>
<div className="sp-card-row-desc">Authentication key for the remote server</div>
</div>
<div className="sp-card-row-control">
<input
className="sp-input w-full"
type="password"
value={draftConfig.api_key}
onChange={(e) => updateDraft({ api_key: e.target.value })}
placeholder="sk-…"
disabled={busyAll}
autoComplete="off"
/>
</div>
</div>
</>
)}
</div>
</div>
</>
)}
{/* ═══════════════════════════════════ ADVANCED SECTION ═══════════════════════ */}
{section === "advanced" && (
<>
<header className="page-header sp-section-header">
<div>
<h2 className="sp-section-title">Advanced</h2>
<p className="sp-section-desc">HuggingFace access and diagnostic information.</p>
</div>
</header>
<div className="settings-cards">
{localError && <div className="sp-error-inline">{localError}</div>}
<div className="sp-card">
{/* HF Token */}
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">HuggingFace token</div>
<div className="sp-card-row-desc">Required for gated or private models</div>
</div>
<div className="sp-card-row-control">
{draftConfig.hugging_face_token && (
<span className="sp-status-pill sp-status-pill--success">Configured</span>
)}
</div>
</div>
<div className="sp-card-divider" />
<div className="sp-card-row">
<div className="sp-card-row-label">
<div className="sp-card-row-title">Token value</div>
<div className="sp-card-row-desc">Generate at huggingface.co/settings/tokens</div>
</div>
<div className="sp-card-row-control">
<input
className="sp-input w-full"
type="password"
value={draftConfig.hugging_face_token || ""}
onChange={(e) => updateDraft({ hugging_face_token: e.target.value })}
placeholder="hf_…"
disabled={busyAll}
autoComplete="off"
/>
</div>
</div>
{/* Diagnostics */}
<div className="sp-card-divider" />
<div className="sp-card-row sp-card-row--full">
<div className="sp-card-row-title">Diagnostics</div>
<dl className="sp-diag-list">
<div className="sp-diag-row">
<dt className="sp-diag-key">Runtime status</dt>
<dd className="sp-diag-val">{snapshot.status}</dd>
</div>
<div className="sp-diag-row">
<dt className="sp-diag-key">Binary path</dt>
<dd className="sp-diag-val sp-mono">
{snapshot.binary_path || "Not found"}
</dd>
</div>
{snapshot.resolved_url && (
<div className="sp-diag-row">
<dt className="sp-diag-key">Resolved endpoint</dt>
<dd className="sp-diag-val sp-accent">{snapshot.resolved_url}</dd>
</div>
)}
{snapshot.last_error && (
<div className="sp-diag-row">
<dt className="sp-diag-key">Last error</dt>
<dd className="sp-diag-val sp-danger">{snapshot.last_error.message}</dd>
</div>
)}
</dl>
</div>
</div>
</div>
</>
)}
</div>
</div>
<div className="settings-action-bar">
<div className="action-bar-inner">
<button
type="button"
className="sp-btn sp-btn--ghost btn-discard"
onClick={resetDraft}
disabled={busyAll || !draftDirty}
>
Discard
</button>
<button
type="button"
className="sp-btn sp-btn--secondary btn-save"
onClick={() => void handleSave()}
disabled={busyAll || !draftDirty}
>
{saving ? "Saving…" : "Save"}
</button>
{isLocal && !isRunning && (
<button
type="button"
className="sp-btn sp-btn--primary btn-save"
onClick={() => void handleSaveAndStart()}
disabled={busyAll || !Boolean(draftConfig.model_path)}
>
{saving ? "Starting…" : "Save & Start"}
</button>
)}
{isLocal && isRunning && (
<button
type="button"
className="sp-btn sp-btn--danger btn-stop"
onClick={() => void handleStop()}
disabled={busyAll || isStopping}
>
{isStopping ? "Stopping…" : "Stop runtime"}
</button>
)}
</div>
</div>
</div>
</div>
);
}