forked from enderofwings/NexusOS
feat(models): skip install-time pull; hardware-aware picks in Models tab
Windows installer no longer auto-downloads models; points to the Models tab. synapse/hardware.py detects RAM + best-effort VRAM and a curated catalog; GET /models/recommended annotates each model with fit (gpu/ram/no); the Models page shows detected RAM/VRAM with fit badges and per-row Pull buttons. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ export function Models({ onPullStateChange }) {
|
||||
const [pulling, setPulling] = useState(false);
|
||||
const [modelName, setModelName] = useState("");
|
||||
const [pullProgress, setPullProgress] = useState("");
|
||||
const [recommended, setRecommended] = useState(null); // {hardware, models}
|
||||
|
||||
const checkOllamaStatus = useCallback(async () => {
|
||||
try {
|
||||
@@ -42,8 +43,9 @@ export function Models({ onPullStateChange }) {
|
||||
}
|
||||
}, [checkOllamaStatus]);
|
||||
|
||||
const pullModel = async () => {
|
||||
if (!modelName.trim()) {
|
||||
const pullModel = async (nameArg) => {
|
||||
const name = (typeof nameArg === "string" ? nameArg : modelName).trim();
|
||||
if (!name) {
|
||||
setError("Please enter a model name");
|
||||
return;
|
||||
}
|
||||
@@ -51,13 +53,13 @@ export function Models({ onPullStateChange }) {
|
||||
setPulling(true);
|
||||
onPullStateChange?.(true);
|
||||
setError("");
|
||||
setPullProgress("");
|
||||
setPullProgress(`Pulling ${name}…`);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/models/pull`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: modelName.trim() }),
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to pull model");
|
||||
@@ -122,11 +124,19 @@ export function Models({ onPullStateChange }) {
|
||||
|
||||
useEffect(() => {
|
||||
loadModels();
|
||||
fetch(`${API_BASE}/models/recommended`).then(r => r.ok ? r.json() : null).then(setRecommended).catch(() => {});
|
||||
const interval = setInterval(loadModels, 30000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [loadModels]);
|
||||
|
||||
const installedNames = new Set(models.map(m => m.name.toLowerCase()));
|
||||
const FIT = {
|
||||
gpu: { label: "🟢 fits GPU", color: "#8aff8a" },
|
||||
ram: { label: "🟡 runs on RAM (CPU)", color: "#e8c65a" },
|
||||
no: { label: "🔴 too big", color: "#ff8a80" },
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", height: "100%", flexGrow: 1 }}>
|
||||
{error && (
|
||||
@@ -215,6 +225,40 @@ export function Models({ onPullStateChange }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recommended for your hardware */}
|
||||
{recommended && (
|
||||
<div style={{ marginBottom: "1.5rem", paddingBottom: "1rem", borderBottom: "1px solid #333" }}>
|
||||
<h3 style={{ margin: "0 0 0.25rem 0", fontSize: "0.95rem", color: "#bbb" }}>Recommended for your hardware</h3>
|
||||
<div style={{ fontSize: "0.78rem", color: "#777", marginBottom: "0.75rem" }}>
|
||||
{recommended.hardware.ram_gb ? `${recommended.hardware.ram_gb} GB RAM` : "RAM unknown"}
|
||||
{" · "}
|
||||
{recommended.hardware.vram_gb ? `${recommended.hardware.vram_gb} GB GPU` : "GPU VRAM unknown — sized by RAM"}
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "0.35rem" }}>
|
||||
{recommended.models.map((m) => {
|
||||
const installed = installedNames.has(m.name.toLowerCase());
|
||||
const fit = FIT[m.fit] || FIT.no;
|
||||
return (
|
||||
<div key={m.name} style={{ display: "flex", alignItems: "center", gap: "0.6rem", padding: "0.4rem 0.6rem", background: "#0f0f0f", border: "1px solid #262626", borderRadius: "8px" }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<span style={{ color: "#eee", fontSize: "0.85rem" }}>{m.name}</span>
|
||||
<span style={{ color: "#666", fontSize: "0.75rem" }}> · {m.params} · {m.size_gb} GB · {m.role}</span>
|
||||
<div style={{ color: "#777", fontSize: "0.72rem" }}>{m.note}</div>
|
||||
</div>
|
||||
<span style={{ color: fit.color, fontSize: "0.75rem", whiteSpace: "nowrap" }}>{fit.label}</span>
|
||||
{installed
|
||||
? <span style={{ color: "#8aff8a", fontSize: "0.75rem", whiteSpace: "nowrap" }}>✓ installed</span>
|
||||
: <button onClick={() => pullModel(m.name)} disabled={pulling}
|
||||
style={{ padding: "0.3rem 0.7rem", background: pulling ? "#555" : "#28a745", color: "#fff", border: "none", borderRadius: "6px", cursor: pulling ? "not-allowed" : "pointer", fontSize: "0.75rem", whiteSpace: "nowrap" }}>
|
||||
Pull
|
||||
</button>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Local Models List */}
|
||||
<div style={{ flexGrow: 1, overflowY: "auto", paddingRight: "0.5rem" }}>
|
||||
<h3 style={{ margin: "0 0 1rem 0", fontSize: "0.95rem", color: "#bbb" }}>
|
||||
|
||||
Reference in New Issue
Block a user