Files
ForgeLM/tauri-app/src/features/runtime/components/RuntimeSettingsDrawer.tsx
T

963 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useRef, useState } from "react";
import {
getModelLibraryState,
pickRuntimeModelFiles,
pickRuntimeModelsDir,
queryRuntimeGpu,
scanRuntimeModels,
searchHuggingFaceModels,
} from "../api";
import {
chooseModel,
createRuntimeConfigPatch,
dedupeModels,
getLocalRuntimeStartReason,
pathName,
resolveDraftModelSelection,
toModelFile,
} from "../runtime-settings-form";
import {
createRuntimeDraftFieldState,
type ModelLibraryState,
type HuggingFaceModelResult,
type RuntimeConfig,
type RuntimeDraftFieldKey,
type RuntimeDraftFieldMeta,
type RuntimeDraftFieldState,
type RuntimeGpuInfo,
type RuntimeLifecycleResult,
type RuntimeModelFile,
type RuntimeSnapshot,
} from "../types";
import "./RuntimeSettingsDrawer.css";
const CTX_PRESETS = [2048, 4096, 8192, 16384, 32768, 65536];
const REMOTE_PRESET_TEMPLATES = [
{ id: "openai", label: "OpenAI", provider: "open_ai", baseUrl: "https://api.openai.com/v1" },
{ id: "openrouter", label: "OpenRouter", provider: "open_router", baseUrl: "https://openrouter.ai/api/v1" },
{ id: "anthropic", label: "Anthropic", provider: "anthropic", baseUrl: "https://api.anthropic.com/v1" },
] as const;
interface Props {
open: boolean;
snapshot: RuntimeSnapshot;
busy: boolean;
activeLifecycleCommand?: RuntimeSnapshot["active_command"];
lastLifecycleResult?: RuntimeLifecycleResult;
lifecycleBusy?: boolean;
applyImpact?: "immediate" | "restart_required" | "save_before_apply";
onClose: () => void;
onSave: (patch: Partial<RuntimeConfig>) => Promise<RuntimeSnapshot>;
onStart: () => Promise<unknown>;
onStop: () => Promise<unknown>;
draftConfig?: RuntimeConfig;
savedBaseline?: RuntimeConfig;
draftDirty?: boolean;
draftFields?: RuntimeDraftFieldState;
onUpdateDraft?: (patch: Partial<RuntimeConfig>) => void;
onUpdateDraftFieldMeta?: (field: RuntimeDraftFieldKey, patch: Partial<RuntimeDraftFieldMeta>) => void;
onResetDraft?: () => void;
}
function valuesEqual(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function maskSecret(value: string): string {
if (!value.trim()) return "Not configured";
return `${"*".repeat(Math.max(4, Math.min(12, value.trim().length)))} (${value.trim().length} chars)`;
}
function formatReferenceValue(field: keyof RuntimeConfig, value: RuntimeConfig[keyof RuntimeConfig]): string {
switch (field) {
case "connection_mode":
return value === "remote" ? "Remote API" : "Local API";
case "start_policy":
return value === "on_app_launch" ? "Start on app launch" : "Start manually";
case "server_url":
return String(value || "Not configured");
case "api_key":
return maskSecret(String(value || ""));
case "model_path":
return value ? pathName(String(value)) : "No model selected";
case "models_dir":
return String(value || "No folder selected");
case "addr":
return `http://${String(value || "127.0.0.1:8080")}`;
case "gpu_layers":
return `${String(value)} GPU layers`;
case "ctx_size":
return `${String(value)} context tokens`;
case "selected_model_id":
return String(value || "No managed model selected");
case "hugging_face_token":
return maskSecret(String(value || ""));
default:
return Array.isArray(value) ? `${value.length} configured` : String(value || "-");
}
}
export default function RuntimeSettingsDrawer({
open,
snapshot,
busy,
activeLifecycleCommand = snapshot.active_command,
lastLifecycleResult = snapshot.last_command_result,
lifecycleBusy = false,
applyImpact = "immediate",
onClose,
onSave,
onStart,
onStop,
draftConfig: controlledDraftConfig,
savedBaseline: controlledSavedBaseline,
draftDirty: controlledDraftDirty,
draftFields: controlledDraftFields,
onUpdateDraft,
onUpdateDraftFieldMeta,
onResetDraft,
}: Props) {
const draftConfig = controlledDraftConfig ?? snapshot.config;
const savedBaseline = controlledSavedBaseline ?? snapshot.config;
const draftFields = controlledDraftFields ?? createRuntimeDraftFieldState();
const draftDirty = controlledDraftDirty ?? !valuesEqual(draftConfig, savedBaseline);
const draftConfigRef = useRef(draftConfig);
const requestIdsRef = useRef<Record<string, number>>({});
const [scannedModels, setScannedModels] = useState<RuntimeModelFile[]>([]);
const [gpuInfo, setGpuInfo] = useState<RuntimeGpuInfo | null>(null);
const [gpuError, setGpuError] = useState("");
const [localError, setLocalError] = useState("");
const [scanning, setScanning] = useState(false);
const [hfQuery, setHfQuery] = useState("");
const [hfResults, setHfResults] = useState<HuggingFaceModelResult[]>([]);
const [hfLoading, setHfLoading] = useState(false);
const [libraryState, setLibraryState] = useState<ModelLibraryState>({ entries: [], managed_dir: "" });
const [confirmDiscard, setConfirmDiscard] = useState(false);
useEffect(() => {
draftConfigRef.current = draftConfig;
}, [draftConfig]);
const updateDraft = (patch: Partial<RuntimeConfig>) => {
onUpdateDraft?.(patch);
};
const updateDraftFieldMeta = (field: RuntimeDraftFieldKey, patch: Partial<RuntimeDraftFieldMeta>) => {
onUpdateDraftFieldMeta?.(field, patch);
};
const beginRequest = (key: string) => {
const nextId = (requestIdsRef.current[key] ?? 0) + 1;
requestIdsRef.current[key] = nextId;
return nextId;
};
const isLatestRequest = (key: string, requestId: number) => requestIdsRef.current[key] === requestId;
const extraModels = useMemo(
() => draftConfig.extra_model_paths.map(toModelFile),
[draftConfig.extra_model_paths],
);
const allModels = useMemo(() => {
const merged = dedupeModels([...scannedModels, ...extraModels]);
if (draftConfig.model_path && !merged.some((model) => model.path === draftConfig.model_path)) {
return [toModelFile(draftConfig.model_path), ...merged];
}
return merged;
}, [draftConfig.model_path, extraModels, scannedModels]);
const dirtyFieldCount = useMemo(() => {
const trackedFields: Array<keyof RuntimeConfig> = [
"connection_mode",
"server_url",
"api_key",
"models_dir",
"model_path",
"addr",
"ctx_size",
"gpu_layers",
"start_policy",
"hugging_face_token",
];
return trackedFields.filter((field) => !valuesEqual(draftConfig[field], savedBaseline[field])).length;
}, [draftConfig, savedBaseline]);
const hasInvalidDraft = Object.values(draftFields).some((meta) => meta?.invalid);
const hasPendingDraft = Object.values(draftFields).some((meta) => meta?.pending);
const hasBlockingDraftState = hasInvalidDraft || hasPendingDraft;
const hasPendingModeChange = draftConfig.connection_mode !== savedBaseline.connection_mode;
const restartRequiredWhileRunning =
draftDirty && draftConfig.connection_mode === "local" && snapshot.process_running && applyImpact === "restart_required";
const maxLayers = gpuInfo?.recommended_layers ?? 200;
const startReason = getLocalRuntimeStartReason({
mode: draftConfig.connection_mode,
binaryPath: snapshot.binary_path,
selectedModel: draftConfig.model_path || "",
});
const isFieldDirty = (field: keyof RuntimeConfig) => !valuesEqual(draftConfig[field], savedBaseline[field]);
const fieldMeta = (field: RuntimeDraftFieldKey) => draftFields[field] ?? { invalid: false, pending: false, message: null };
const showLifecycleResult = lastLifecycleResult.command !== "idle" && !lifecycleBusy;
const applyGuidance = !draftDirty || draftConfig.connection_mode !== "local"
? null
: hasPendingModeChange || applyImpact === "save_before_apply"
? "Stop the local API or save first before applying this change."
: applyImpact === "restart_required"
? "Restart required to apply saved changes."
: "Applies immediately after save.";
const saveLabel = activeLifecycleCommand === "save" ? "Saving changes..." : "Save changes";
const tertiaryLabel = hasPendingModeChange
? "Save mode before applying"
: activeLifecycleCommand === "start"
? "Starting local API..."
: activeLifecycleCommand === "stop"
? "Stopping local API..."
: restartRequiredWhileRunning
? "Restart required to apply saved changes"
: snapshot.process_running
? "Stop local API"
: "Save and start local API";
const renderFieldState = (field: RuntimeDraftFieldKey) => {
const meta = fieldMeta(field);
const dirty = isFieldDirty(field);
if (!dirty && !meta.invalid && !meta.pending) {
return null;
}
return (
<div className={`runtime-field-state${dirty ? " is-draft" : ""}${meta.invalid ? " is-invalid" : ""}`}>
<div className="runtime-field-state-row">
{dirty && <span className="runtime-field-badge">Draft</span>}
{meta.invalid && <span className="runtime-field-badge runtime-field-badge-danger">Invalid</span>}
{meta.pending && <span className="runtime-field-badge runtime-field-badge-pending">Pending</span>}
{dirty && (
<>
<span className="runtime-field-state-label">Live now</span>
<span className="runtime-field-state-value">{formatReferenceValue(field, savedBaseline[field])}</span>
</>
)}
</div>
{meta.message && <div className="runtime-field-error-text">{meta.message}</div>}
</div>
);
};
const applyResolvedModelState = (models: RuntimeModelFile[]) => {
const currentDraft = draftConfigRef.current;
const resolved = resolveDraftModelSelection(currentDraft.model_path || "", models, currentDraft.extra_model_paths.map(toModelFile));
updateDraft({
model_path: resolved.nextModel || null,
selected_model_id:
resolved.invalid || resolved.nextModel === currentDraft.model_path
? currentDraft.selected_model_id
: null,
});
updateDraftFieldMeta("model_path", {
invalid: resolved.invalid,
pending: false,
message: resolved.message,
});
};
const loadModelLibrary = async () => {
const requestId = beginRequest("library");
try {
const state = await getModelLibraryState();
if (!isLatestRequest("library", requestId)) return;
setLibraryState(state);
} catch (error) {
if (!isLatestRequest("library", requestId)) return;
setLocalError(String(error));
}
};
const loadGpuInfo = async () => {
const requestId = beginRequest("gpu");
try {
const info = await queryRuntimeGpu();
if (!isLatestRequest("gpu", requestId)) return;
setGpuInfo(info);
setGpuError("");
if (info && draftConfigRef.current.gpu_layers === 0) {
updateDraft({ gpu_layers: info.recommended_layers });
}
} catch (error) {
if (!isLatestRequest("gpu", requestId)) return;
setGpuError(String(error));
}
};
const handleRescan = async (dir = draftConfigRef.current.models_dir || "") => {
if (!dir.trim()) return;
const requestId = beginRequest("model-selection");
setScanning(true);
setLocalError("");
updateDraftFieldMeta("models_dir", { pending: true, invalid: false, message: null });
updateDraftFieldMeta("model_path", { pending: true });
try {
const models = await scanRuntimeModels(dir.trim());
if (!isLatestRequest("model-selection", requestId)) return;
setScannedModels(models);
applyResolvedModelState(models);
updateDraftFieldMeta("models_dir", { pending: false, invalid: false, message: null });
} catch (error) {
if (!isLatestRequest("model-selection", requestId)) return;
setLocalError(String(error));
setScannedModels([]);
updateDraftFieldMeta("models_dir", { pending: false });
updateDraftFieldMeta("model_path", { pending: false });
} finally {
if (isLatestRequest("model-selection", requestId)) {
setScanning(false);
}
}
};
const handlePickDir = async () => {
const requestId = beginRequest("model-selection");
setLocalError("");
updateDraftFieldMeta("models_dir", { pending: true, invalid: false, message: null });
updateDraftFieldMeta("model_path", { pending: true });
try {
const result = await pickRuntimeModelsDir();
if (!isLatestRequest("model-selection", requestId) || !result) return;
updateDraft({ models_dir: result.dir });
setScannedModels(result.models);
applyResolvedModelState(result.models);
updateDraftFieldMeta("models_dir", { pending: false, invalid: false, message: null });
} catch (error) {
if (!isLatestRequest("model-selection", requestId)) return;
setLocalError(String(error));
updateDraftFieldMeta("models_dir", { pending: false });
updateDraftFieldMeta("model_path", { pending: false });
}
};
const handlePickFiles = async () => {
const requestId = beginRequest("model-selection");
setLocalError("");
updateDraftFieldMeta("model_path", { pending: true });
try {
const picked = await pickRuntimeModelFiles();
if (!isLatestRequest("model-selection", requestId) || picked.length === 0) {
updateDraftFieldMeta("model_path", { pending: false });
return;
}
const currentDraft = draftConfigRef.current;
const nextExtra = dedupeModels([...currentDraft.extra_model_paths.map(toModelFile), ...picked]);
const nextModel = currentDraft.model_path || nextExtra[0]?.path || "";
updateDraft({
extra_model_paths: nextExtra.map((model) => model.path),
model_path: nextModel || null,
selected_model_id: currentDraft.model_path ? currentDraft.selected_model_id : null,
});
updateDraftFieldMeta("model_path", { pending: false, invalid: false, message: null });
} catch (error) {
if (!isLatestRequest("model-selection", requestId)) return;
setLocalError(String(error));
updateDraftFieldMeta("model_path", { pending: false });
}
};
const handleHubSearch = async () => {
if (!hfQuery.trim()) {
setHfResults([]);
return;
}
const requestId = beginRequest("hf-search");
setLocalError("");
setHfLoading(true);
try {
const results = await searchHuggingFaceModels(hfQuery.trim(), draftConfig.hugging_face_token.trim());
if (!isLatestRequest("hf-search", requestId)) return;
setHfResults(results);
} catch (error) {
if (!isLatestRequest("hf-search", requestId)) return;
setLocalError(String(error));
setHfResults([]);
} finally {
if (isLatestRequest("hf-search", requestId)) {
setHfLoading(false);
}
}
};
const handleSave = async () => {
setLocalError("");
try {
await onSave(buildPatch());
setConfirmDiscard(false);
} catch (error) {
setLocalError(String(error));
}
};
const handleSaveAndStart = async () => {
setLocalError("");
if (snapshot.process_running) {
try {
await onStop();
} catch (error) {
setLocalError(String(error));
}
return;
}
try {
const next = await onSave(buildPatch());
if (next.config.connection_mode !== "local") return;
if (!next.config.model_path?.trim()) return;
if (next.last_error?.code === "restart_required") return;
await onStart();
} catch (error) {
setLocalError(String(error));
}
};
useEffect(() => {
if (!open) return;
setConfirmDiscard(false);
setLocalError("");
setHfResults([]);
void loadModelLibrary();
void loadGpuInfo();
if (draftConfig.connection_mode === "local" && draftConfig.models_dir?.trim()) {
void handleRescan(draftConfig.models_dir);
}
}, [open]);
if (!open) return null;
const buildPatch = (): RuntimeConfig => ({
...createRuntimeConfigPatch(
savedBaseline,
{
mode: draftConfig.connection_mode,
startPolicy: draftConfig.start_policy,
serverUrl: draftConfig.server_url,
apiKey: draftConfig.api_key,
modelsDir: draftConfig.models_dir || "",
selectedModel: draftConfig.model_path || "",
selectedModelId: draftConfig.selected_model_id,
addr: draftConfig.addr,
gpuLayers: draftConfig.gpu_layers,
ctxSize: draftConfig.ctx_size,
extraModels,
},
maxLayers,
),
hugging_face_token: draftConfig.hugging_face_token.trim(),
remote_presets: draftConfig.remote_presets?.length
? draftConfig.remote_presets
: REMOTE_PRESET_TEMPLATES.map((preset) => ({
id: preset.id,
label: preset.label,
provider: preset.provider,
base_url: preset.baseUrl,
default_model: "",
})),
});
return (
<div className="runtime-drawer-shell" role="dialog" aria-modal="true" aria-label="Draft settings">
<button
className="runtime-drawer-backdrop"
onClick={() => {
setConfirmDiscard(false);
onClose();
}}
aria-label="Close runtime settings"
/>
<aside className="runtime-drawer">
<div className="runtime-drawer-header runtime-drawer-header-sticky">
<div>
<div className="runtime-drawer-kicker">Draft settings</div>
<h2>Draft settings</h2>
<div className="runtime-drawer-summary-row">
<span className={`runtime-drawer-summary-badge${draftDirty ? " is-dirty" : ""}`}>
{draftDirty ? `${dirtyFieldCount} unsaved ${dirtyFieldCount === 1 ? "change" : "changes"}` : "No draft changes yet"}
</span>
<span className="runtime-live-chip">Live now</span>
<span className="runtime-live-chip-value">
{snapshot.status === "ready"
? "Local API ready"
: snapshot.config.connection_mode === "remote"
? "Remote API mode"
: "Local API setup"}
</span>
</div>
<p className="runtime-drawer-copy">
Edit runtime settings as a draft. Close keeps this draft in-session until you save or discard it.
</p>
</div>
<button
className="runtime-drawer-close"
onClick={() => {
setConfirmDiscard(false);
onClose();
}}
type="button"
aria-label="Close runtime settings"
>
×
</button>
</div>
<div className="runtime-drawer-body">
<section className="runtime-overview-card">
<div>
<div className="runtime-overview-label">Current status</div>
<div className="runtime-overview-value">
{snapshot.status === "ready"
? "Local API ready"
: snapshot.config.connection_mode === "remote"
? "Remote API mode"
: "Local API setup"}
</div>
</div>
<div>
<div className="runtime-overview-label">Endpoint</div>
<div className="runtime-overview-value runtime-overview-mono">
{snapshot.config.connection_mode === "remote"
? snapshot.config.server_url || "Not configured"
: `http://${snapshot.config.addr}`}
</div>
</div>
<div>
<div className="runtime-overview-label">Saved model</div>
<div className="runtime-overview-value">
{savedBaseline.model_path ? pathName(savedBaseline.model_path) : "No model saved"}
</div>
</div>
</section>
<section className="runtime-drawer-section">
<div className="runtime-drawer-section-title">Connection mode</div>
<div className="runtime-segmented" role="tablist" aria-label="Connection mode">
<button
className={`runtime-segmented-btn${draftConfig.connection_mode === "local" ? " active" : ""}${isFieldDirty("connection_mode") ? " is-draft" : ""}`}
onClick={() => updateDraft({ connection_mode: "local" })}
type="button"
>
Local API
</button>
<button
className={`runtime-segmented-btn${draftConfig.connection_mode === "remote" ? " active" : ""}${isFieldDirty("connection_mode") ? " is-draft" : ""}`}
onClick={() => updateDraft({ connection_mode: "remote" })}
type="button"
>
Remote API
</button>
</div>
{isFieldDirty("connection_mode") && (
<div className="runtime-field-state is-draft">
<div className="runtime-field-state-row">
<span className="runtime-field-badge">Draft</span>
<span className="runtime-field-state-label">Live now</span>
<span className="runtime-field-state-value">
{formatReferenceValue("connection_mode", savedBaseline.connection_mode)}
</span>
</div>
</div>
)}
</section>
{draftConfig.connection_mode === "remote" ? (
<section className="runtime-drawer-section runtime-section-card">
<div className="runtime-field-label">Provider presets</div>
<div className="runtime-preset-row runtime-preset-row-first">
{REMOTE_PRESET_TEMPLATES.map((preset) => (
<button
key={preset.id}
className={`runtime-preset${draftConfig.server_url.trim() === preset.baseUrl ? " active" : ""}`}
onClick={() => updateDraft({ server_url: preset.baseUrl })}
type="button"
>
{preset.label}
</button>
))}
</div>
<label className="runtime-field-label" htmlFor="runtime-remote-url">Remote server URL</label>
<input
id="runtime-remote-url"
className={`runtime-field-input${isFieldDirty("server_url") ? " is-draft" : ""}${fieldMeta("server_url").invalid ? " is-invalid" : ""}`}
value={draftConfig.server_url}
onChange={(event) => updateDraft({ server_url: event.target.value })}
placeholder="https://api.example.com"
type="text"
/>
{renderFieldState("server_url")}
<label className="runtime-field-label" htmlFor="runtime-api-key">API key</label>
<input
id="runtime-api-key"
className={`runtime-field-input${isFieldDirty("api_key") ? " is-draft" : ""}${fieldMeta("api_key").invalid ? " is-invalid" : ""}`}
value={draftConfig.api_key}
onChange={(event) => updateDraft({ api_key: event.target.value })}
placeholder="sk-..."
type="password"
/>
{renderFieldState("api_key")}
</section>
) : (
<>
<section className="runtime-drawer-section runtime-section-card runtime-grid-two">
<div>
<div className="runtime-field-label">Managed endpoint</div>
<div className="runtime-static-value runtime-static-value-mono">http://{draftConfig.addr.trim() || snapshot.config.addr}</div>
</div>
<div>
<div className="runtime-field-label">Binary discovery</div>
<div className="runtime-static-value runtime-static-value-path">{snapshot.binary_path || "Binary not found yet"}</div>
</div>
</section>
<section className="runtime-drawer-section runtime-section-card">
<div className="runtime-field-row runtime-field-row-top">
<div>
<div className="runtime-field-label">Managed library</div>
<div className="runtime-field-support">
App-managed downloads live outside your scanned folders so they survive app updates.
</div>
</div>
</div>
<div className="runtime-static-value runtime-static-value-path">
{libraryState.managed_dir || "Loading managed storage path..."}
</div>
{libraryState.entries.length > 0 ? (
<div className="runtime-chip-list">
{libraryState.entries.map((entry) => (
<button
key={entry.id}
className={`runtime-chip runtime-chip-selectable${draftConfig.selected_model_id === entry.id ? " active" : ""}${isFieldDirty("model_path") ? " is-draft" : ""}`}
onClick={() => updateDraft({ model_path: entry.path, selected_model_id: entry.id })}
type="button"
title={entry.path}
>
{entry.label}
</button>
))}
</div>
) : (
<div className="runtime-field-support">No managed downloads yet.</div>
)}
</section>
<section className="runtime-drawer-section runtime-section-card">
<div className="runtime-field-row">
<div>
<div className="runtime-field-label">Models folder</div>
<div className={`runtime-static-value runtime-static-value-path${isFieldDirty("models_dir") ? " is-draft" : ""}${fieldMeta("models_dir").invalid ? " is-invalid" : ""}`}>
{draftConfig.models_dir || "Choose a folder or add individual files"}
</div>
</div>
<div className="runtime-inline-actions">
<button className="runtime-inline-btn" onClick={handlePickDir} type="button">Browse</button>
<button className="runtime-inline-btn" onClick={() => { void handleRescan(); }} type="button" disabled={!draftConfig.models_dir?.trim() || scanning}>
{scanning ? "Scanning..." : "Rescan"}
</button>
<button className="runtime-inline-btn" onClick={handlePickFiles} type="button">Add files</button>
</div>
</div>
{renderFieldState("models_dir")}
<label className="runtime-field-label" htmlFor="runtime-model-select">Selected model</label>
<select
id="runtime-model-select"
className={`runtime-field-input runtime-field-select${isFieldDirty("model_path") ? " is-draft" : ""}${fieldMeta("model_path").invalid ? " is-invalid" : ""}`}
value={draftConfig.model_path || ""}
onChange={(event) =>
updateDraft({
model_path: event.target.value || null,
selected_model_id: null,
})
}
>
<option value="">Choose a model</option>
{allModels.map((model) => (
<option key={model.path} value={model.path}>{model.name}</option>
))}
</select>
{renderFieldState("model_path")}
{extraModels.length > 0 && (
<div className="runtime-chip-list">
{extraModels.map((model) => (
<button
key={model.path}
className="runtime-chip"
onClick={() => {
const next = extraModels.filter((entry) => entry.path !== model.path);
updateDraft({
extra_model_paths: next.map((entry) => entry.path),
model_path:
draftConfig.model_path === model.path
? chooseModel("", scannedModels, next)
: draftConfig.model_path,
selected_model_id: draftConfig.model_path === model.path ? null : draftConfig.selected_model_id,
});
}}
type="button"
title={`Remove ${model.name}`}
>
{model.name}
<span>×</span>
</button>
))}
</div>
)}
</section>
<section className="runtime-drawer-section runtime-section-card">
<div className="runtime-field-row runtime-field-row-top">
<div>
<div className="runtime-field-label">Discover from Hugging Face</div>
<div className="runtime-field-support">
Search public GGUF repos, preview verified files, and keep your local library as the primary runtime source.
</div>
</div>
</div>
<label className="runtime-field-label" htmlFor="runtime-hf-token">Optional Hugging Face token</label>
<input
id="runtime-hf-token"
className={`runtime-field-input${isFieldDirty("hugging_face_token") ? " is-draft" : ""}`}
value={draftConfig.hugging_face_token}
onChange={(event) => updateDraft({ hugging_face_token: event.target.value })}
placeholder="hf_..."
type="password"
/>
<div className="runtime-field-row runtime-field-row-top">
<div className="runtime-search-grow">
<label className="runtime-field-label" htmlFor="runtime-hf-search">Search public GGUF repos</label>
<input
id="runtime-hf-search"
className="runtime-field-input"
value={hfQuery}
onChange={(event) => setHfQuery(event.target.value)}
onInput={(event) => setHfQuery((event.target as HTMLInputElement).value)}
placeholder="phi-4, mistral, qwen..."
type="text"
/>
</div>
<button className="runtime-inline-btn" onClick={() => { void handleHubSearch(); }} type="button" disabled={hfLoading}>
{hfLoading ? "Searching..." : "Search Hub"}
</button>
</div>
{hfResults.length > 0 ? (
<div className="runtime-search-results" aria-label="Hugging Face search results">
{hfResults.map((result) => (
<article key={result.repo_id} className="runtime-search-card">
<div className="runtime-search-title">{result.repo_id}</div>
<div className="runtime-field-support">{result.author || "Unknown author"}</div>
<div className="runtime-verified-file-list">
{result.verified_files.map((file) => (
<div key={`${result.repo_id}:${file.path}`} className="runtime-verified-file">
<span>{file.path}</span>
<span className="runtime-verified-file-meta">
{file.quantization || "gguf"}
{file.size ? ` · ${Math.round(file.size / (1024 * 1024))} MiB` : ""}
</span>
</div>
))}
</div>
</article>
))}
</div>
) : (
<div className="runtime-field-support">
{hfLoading ? "Looking up verified GGUF files..." : "Search for a model family to preview compatible local installs."}
</div>
)}
</section>
<section className="runtime-drawer-section runtime-section-card runtime-grid-two">
<div>
<label className="runtime-field-label" htmlFor="runtime-addr">Listen address</label>
<input
id="runtime-addr"
className={`runtime-field-input${isFieldDirty("addr") ? " is-draft" : ""}`}
value={draftConfig.addr}
onChange={(event) => updateDraft({ addr: event.target.value })}
placeholder="127.0.0.1:8080"
type="text"
/>
</div>
<div>
<label className="runtime-field-label" htmlFor="runtime-context">Context size</label>
<input
id="runtime-context"
className={`runtime-field-input${isFieldDirty("ctx_size") ? " is-draft" : ""}`}
min={512}
step={512}
value={draftConfig.ctx_size}
onChange={(event) => updateDraft({ ctx_size: Math.max(512, parseInt(event.target.value, 10) || 4096) })}
type="number"
/>
</div>
</section>
<section className="runtime-drawer-section runtime-section-card">
<div className="runtime-field-row runtime-field-row-top">
<div>
<div className="runtime-field-label">GPU layers</div>
<div className="runtime-field-support">
{gpuInfo ? `${gpuInfo.name} · recommended ${gpuInfo.recommended_layers}` : gpuError || "GPU detection unavailable"}
</div>
</div>
{gpuInfo && (
<button className="runtime-inline-btn" onClick={() => updateDraft({ gpu_layers: gpuInfo.recommended_layers })} type="button">
Use recommended
</button>
)}
</div>
<input
className={`runtime-field-input${isFieldDirty("gpu_layers") ? " is-draft" : ""}`}
min={0}
max={maxLayers}
value={draftConfig.gpu_layers}
onChange={(event) => updateDraft({ gpu_layers: Math.max(0, Math.min(maxLayers, parseInt(event.target.value, 10) || 0)) })}
type="number"
/>
<div className="runtime-preset-row">
{CTX_PRESETS.map((preset) => (
<button
key={preset}
className={`runtime-preset${draftConfig.ctx_size === preset ? " active" : ""}`}
onClick={() => updateDraft({ ctx_size: preset })}
type="button"
>
{preset >= 1024 ? `${preset / 1024}k` : preset}
</button>
))}
</div>
</section>
<section className="runtime-drawer-section runtime-section-card">
<label className="runtime-checkbox-row">
<input
checked={draftConfig.start_policy === "on_app_launch"}
onChange={(event) => updateDraft({ start_policy: event.target.checked ? "on_app_launch" : "manual" })}
type="checkbox"
/>
<span>Start the local API automatically when the desktop app opens</span>
</label>
</section>
</>
)}
{(localError || snapshot.last_error) && (
<section className="runtime-drawer-section">
<div className="runtime-error-banner">
{localError || snapshot.last_error?.message || "Couldn't save your draft. Review the highlighted fields and try again — your unsaved changes are still here."}
</div>
</section>
)}
{showLifecycleResult && (
<section className="runtime-drawer-section">
<div
className={`runtime-apply-guidance runtime-apply-guidance-${lastLifecycleResult.outcome}`}
role="status"
aria-live="polite"
>
{lastLifecycleResult.message}
</div>
</section>
)}
{applyGuidance && (
<section className="runtime-drawer-section">
<div
className={`runtime-apply-guidance${applyImpact !== "immediate" ? " is-warning" : ""}`}
role="status"
>
{applyGuidance}
</div>
</section>
)}
{!snapshot.process_running && draftConfig.connection_mode === "local" && startReason && (
<section className="runtime-drawer-section">
<div className="runtime-warning-banner">{startReason}</div>
</section>
)}
</div>
<div className="runtime-drawer-footer runtime-drawer-footer-sticky">
<div className="runtime-footer-main-actions">
<button
className="runtime-footer-btn runtime-footer-btn-secondary"
onClick={() => setConfirmDiscard(true)}
type="button"
disabled={busy || lifecycleBusy || !draftDirty}
>
Discard changes
</button>
<button
className="runtime-footer-btn runtime-footer-btn-primary"
onClick={() => { void handleSave(); }}
type="button"
disabled={busy || lifecycleBusy || !draftDirty || hasBlockingDraftState}
>
{saveLabel}
</button>
</div>
{draftConfig.connection_mode === "local" && (
<button
className="runtime-footer-btn runtime-footer-btn-tertiary"
onClick={() => { void handleSaveAndStart(); }}
type="button"
disabled={
busy ||
lifecycleBusy ||
hasBlockingDraftState ||
hasPendingModeChange ||
restartRequiredWhileRunning ||
(!snapshot.process_running && Boolean(startReason))
}
>
{tertiaryLabel}
</button>
)}
</div>
{confirmDiscard && (
<div className="runtime-discard-confirmation">
<div className="runtime-discard-copy">Revert to saved: Replace all unsaved edits with the last saved values.</div>
<div className="runtime-inline-actions">
<button className="runtime-inline-btn" onClick={() => setConfirmDiscard(false)} type="button">
Keep editing
</button>
<button
className="runtime-inline-btn runtime-inline-btn-danger"
onClick={() => {
onResetDraft?.();
setConfirmDiscard(false);
}}
type="button"
>
Revert to saved
</button>
</div>
</div>
)}
</aside>
</div>
);
}