Initial ForgeLM import
This commit is contained in:
commit
a392b9dc60
4296 files changed
+1212675
No files matched your search
@@ -0,0 +1,962 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user