import React, { useCallback, useEffect, useRef, useState } from "react"; import { pickRuntimeModelFiles, pickRuntimeModelsDir, queryRuntimeGpu, searchHuggingFaceModels, getModelLibraryState, swapModel, deleteModelFile, openModelFolder, } from "../runtime/api"; import { dedupeModels, pathName, toModelFile, } from "../runtime/runtime-settings-form"; import { useModelDownload } from "../runtime/useModelDownload"; 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, FolderOpen } 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) => void; saveConfig: (patch: Partial) => Promise; startLocal: () => Promise; stopLocal: () => Promise; scanModels: (dir: string | null) => Promise; resetDraft: () => void; } interface Props { runtime: SettingsRuntime; onClose: () => void; onModelSwapped?: (modelId: string) => 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 formatMiBAsGbMb(mib: number): string { if (!Number.isFinite(mib) || mib <= 0) return "0 MB"; if (mib >= 1024) return `${(mib / 1024).toFixed(2)} GB`; return `${mib.toFixed(1)} MB`; } 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, onModelSwapped }: Props) { const { snapshot, savedBaseline, draftConfig, draftDirty, loading, busy, lifecycleBusy, scannedModels, isScanningModels, updateDraft, saveConfig, startLocal, stopLocal, scanModels, resetDraft, } = runtime; const [section, setSection] = useState
("model"); const [hfQuery, setHfQuery] = useState(""); const [hfResults, setHfResults] = useState([]); const [hfLoading, setHfLoading] = useState(false); const [hfHasSearched, setHfHasSearched] = useState(false); const [gpuInfo, setGpuInfo] = useState(null); const [localError, setLocalError] = useState(""); const [successMessage, setSuccessMessage] = useState(""); const [saving, setSaving] = useState(false); const [isSwapping, setIsSwapping] = useState(false); const [swappingMessage, setSwappingMessage] = useState(""); const didAutoScan = useRef(false); const busyAll = busy || lifecycleBusy || saving || isSwapping; const { activeFile: activeDownloadKey, cancelDownload, didComplete, downloaded, downloadedFilePath, error: downloadError, isDownloading, progress, resetDidComplete, speed, startDownload, total, } = useModelDownload(draftConfig.hugging_face_token || ""); // ── 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]); useEffect(() => { if (downloadError) { setLocalError(downloadError); } }, [downloadError]); useEffect(() => { if (isDownloading) { setSuccessMessage(""); } }, [isDownloading]); useEffect(() => { if (!didComplete) return; const downloadedPath = downloadedFilePath; const modelPath = downloadedPath; getModelLibraryState() .then(({ managed_dir }) => { if (managed_dir) { void scanModels(managed_dir); } return null; }) .catch(() => undefined) .finally(() => { if (modelPath) { console.log("[SettingsPage] Auto-selecionando modelo baixado:", modelPath); updateDraft({ model_path: modelPath }); onModelSwapped?.(modelPath); setSuccessMessage( `Modelo baixado e selecionado com sucesso!\n${modelPath}` ); } resetDidComplete(); }); }, [didComplete, downloadedFilePath]); // ── 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); setHfHasSearched(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 handleDownload = useCallback(async (repo: string, filePath: string) => { setLocalError(""); try { await startDownload(repo, filePath); } catch (error) { setLocalError(String(error)); } }, [startDownload]); const handleCancelDownload = useCallback(async () => { setLocalError(""); try { await cancelDownload(); } catch (error) { setLocalError(String(error)); } }, [cancelDownload]); const handleModelSwap = useCallback(async (newModelPath: string) => { if (isSwapping || isDownloading) return; setLocalError(""); setSuccessMessage(""); setIsSwapping(true); setSwappingMessage(`Trocando para ${pathName(newModelPath)}…`); try { await swapModel(newModelPath); updateDraft({ model_path: newModelPath }); onModelSwapped?.(newModelPath); setSwappingMessage("Modelo trocado com sucesso."); setTimeout(() => setSwappingMessage(""), 3000); } catch (error) { setLocalError(String(error)); } finally { setIsSwapping(false); } }, [isSwapping, isDownloading, updateDraft, onModelSwapped]); const handleOpenFolder = useCallback(async (filePath: string) => { try { await openModelFolder(filePath); } catch (error) { setLocalError(String(error)); } }, []); const handleDeleteModel = useCallback(async (filePath: string) => { setLocalError(""); try { await deleteModelFile(filePath); runtime.scanModels(draftConfig.models_dir ?? null); } catch (error) { setLocalError(String(error)); } }, [runtime, draftConfig.models_dir]); 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 (
{/* ── Left navigation ──────────────────────────────────────────── */} {/* ── Main area (right side) ─────────────────────────────────── */}
{/* ═══════════════════════════════════ MODEL SECTION ═══════════════════════════ */} {section === "model" && ( <>

Model

Select a local GGUF model file for inference.

{isRunning && ( {isStopping ? "Stopping local API" : getRuntimeStateLabel(snapshot)} )}
{localError &&
{localError}
} {/* Active model */}
{snapshot.config.model_path ? (
Active model
Currently loaded GGUF file
{pathName(snapshot.config.model_path)}
{snapshot.config.model_path}
{draftDirty && draftConfig.model_path !== snapshot.config.model_path && (
Draft → {pathName(draftConfig.model_path) || "None selected"}
)}
) : (
Active model
No model selected yet

Use the sections below to load one.

)} {/* Scan folder */}
Scan folder
Browse and scan a directory for .gguf files
{draftConfig.models_dir && ( )}
{draftConfig.models_dir && (
{draftConfig.models_dir}
)}
{/* Model list from scan */} {(isScanningModels || scannedModels.length > 0) && ( <>
{isScanningModels && scannedModels.length === 0 && (
Scanning for models…
)}
{scannedModels.map((model) => { const selected = draftConfig.model_path === model.path; return ( ); })}
)} {/* Browse individual files */}
Individual files
Add specific .gguf files without scanning a folder
{draftConfig.extra_model_paths.length > 0 && (
{draftConfig.extra_model_paths.map((p) => { const selected = draftConfig.model_path === p; return (
); })}
)}
{/* HuggingFace search */}
Search HuggingFace
{!draftConfig.hugging_face_token && "Add HF token in Advanced for private models"}
O que é Quantização?

É a compressão do modelo para que ele rode mais rápido e caiba na memória (RAM/VRAM) do seu PC.

Máxima Qualidade (Pesado): Q8_0, Q6_K — Quase sem perda de inteligência. Equilíbrio Ideal (Recomendado): Q4_K_M, Q5_K_M — Melhor custo-benefício. Alta Compressão (Leve): Q2_K, Q3_K_M — Roda em PCs fracos, mas o modelo fica mais "esquecido" ou impreciso.
{ setHfQuery(e.target.value); setHfHasSearched(false); }} onKeyDown={(e) => { if (e.key === "Enter") void handleHfSearch(); }} />
{successMessage && (
✓

{successMessage.split("\n")[0]}

{successMessage.split("\n")[1]}

)} {/* Hot-swap loading indicator */} {isSwapping && (
{swappingMessage}
)} {/* ── Hub: Local library (empty query) ── */} {!hfQuery.trim() && scannedModels.length > 0 && (

Modelos Locais ({scannedModels.length})

{scannedModels.map((model) => { const isSelected = draftConfig.model_path === model.path; const modelSize = model.size ? `${(model.size / 1_073_741_824).toFixed(1)} GB` : "—"; return (
!isSwapping && !isDownloading && handleModelSwap(model.path)} > {pathName(model.path)}
{modelSize}
); })}
)} {!hfQuery.trim() && scannedModels.length === 0 && !isScanningModels && (

Nenhum modelo local. Pesquise no HuggingFace ou configure a pasta de modelos.

)} {/* ── Hub: HuggingFace search results ── */} {hfQuery.trim() && hfResults.length > 0 && (
{hfResults.map((result) => { const localNames = new Set(scannedModels.map((m) => pathName(m.path))); return (
{result.repo_id}
{result.author || "Unknown author"}
{result.verified_files.length > 0 && (
{result.verified_files.map((file) => { const downloadKey = `${result.repo_id}:${file.path}`; const isActiveDownload = activeDownloadKey === downloadKey; const fileName = file.path.replace(/\.gguf$/i, ""); const fileSize = file.size ? `${(file.size / 1_073_741_824).toFixed(1)} GB` : "—"; const quantMatch = file.path.replace(/\.gguf$/i, "").split(/[-.]/).pop() ?? "gguf"; const isLocalDuplicate = localNames.has(fileName); const localModel = scannedModels.find( (m) => pathName(m.path) === fileName, ); let actionButtons: React.ReactNode; if (isActiveDownload && isDownloading) { actionButtons = ( <>
{Number.isFinite(speed) ? speed.toFixed(1) : "0.0"} MB/s {formatMiBAsGbMb(downloaded)} / {formatMiBAsGbMb(total)}
); } else if (isLocalDuplicate) { actionButtons = (
); } else { actionButtons = ( ); } return (
{fileName}
{fileSize} {quantMatch} {actionButtons}
); })}
)}
); })}
)} {hfHasSearched && !hfLoading && hfQuery && hfResults.length === 0 && (

Nenhum resultado. Tente outra busca.

)}
)} {/* ═══════════════════════════════════ RUNTIME SECTION ════════════════════════ */} {section === "runtime" && ( <>

Runtime

GPU acceleration, context window, and startup policy.

{localError &&
{localError}
}
{/* GPU acceleration */}
GPU acceleration
{gpuInfo ? `${gpuInfo.name} · ${gpuInfo.vram_mib} MiB VRAM` : "No GPU detected — CPU only"}
{gpuInfo && ( GPU detected )}
GPU layers
Layers to offload to the GPU
{draftConfig.gpu_layers}
updateDraft({ gpu_layers: Number(e.target.value) })} className="sp-slider w-full mt-2" disabled={busyAll} />
{gpuInfo && ( )}
{/* Context window */}
Context window
Maximum tokens the model can process
{draftConfig.ctx_size.toLocaleString()} tokens
{CTX_PRESETS.map((preset) => ( ))}
{ const v = Number(e.target.value); if (v >= MIN_CTX_SIZE) updateDraft({ ctx_size: v }); }} disabled={busyAll} /> Min {MIN_CTX_SIZE.toLocaleString()}
{/* Startup policy */}
Startup policy
When to start the local API
{(["manual", "on_app_launch"] as const).map((policy) => { const active = draftConfig.start_policy === policy; return ( ); })}
{draftConfig.start_policy === "manual" ? "Start the local API from the runtime panel when needed." : "Automatically start the local API when ForgeLM opens."}
)} {/* ═══════════════════════════════════ CONNECTION SECTION ═════════════════════ */} {section === "connection" && ( <>

Connection

Use the built-in local inference engine or connect to a remote OpenAI-compatible server.

{localError &&
{localError}
}
{/* Mode */}
Mode
{draftConfig.connection_mode === "local" ? "Runs the inference server locally on your machine." : "Forwards all requests to an external OpenAI-compatible endpoint."}
{(["local", "remote"] as const).map((mode) => { const active = draftConfig.connection_mode === mode; return ( ); })}
{/* Local settings */} {draftConfig.connection_mode === "local" && ( <>
Bind address
Host and port for the local API
updateDraft({ addr: e.target.value })} placeholder="127.0.0.1:8080" disabled={busyAll} />

Available at{" "} http://{draftConfig.addr || "127.0.0.1:8080"}

)} {/* Remote settings */} {draftConfig.connection_mode === "remote" && ( <>
Server URL
Base URL of the remote API
updateDraft({ server_url: e.target.value })} placeholder="https://api.openai.com/v1" disabled={busyAll} />
API key
Authentication key for the remote server
updateDraft({ api_key: e.target.value })} placeholder="sk-…" disabled={busyAll} autoComplete="off" />
)}
)} {/* ═══════════════════════════════════ ADVANCED SECTION ═══════════════════════ */} {section === "advanced" && ( <>

Advanced

HuggingFace access and diagnostic information.

{localError &&
{localError}
}
{/* HF Token */}
HuggingFace token
Required for gated or private models
{draftConfig.hugging_face_token && ( Configured )}
Token value
Generate at huggingface.co/settings/tokens
updateDraft({ hugging_face_token: e.target.value })} placeholder="hf_…" disabled={busyAll} autoComplete="off" />
{/* Diagnostics */}
Diagnostics
Runtime status
{snapshot.status}
Binary path
{snapshot.binary_path || "Not found"}
{snapshot.resolved_url && (
Resolved endpoint
{snapshot.resolved_url}
)} {snapshot.last_error && (
Last error
{snapshot.last_error.message}
)}
)}
{isLocal && !isRunning && ( )} {isLocal && isRunning && ( )}
); }