refactor: massive UX and beginner refactoring (Cookbook, Dashboard, Layout, Wording)
This commit is contained in:
+206
-154
@@ -1,17 +1,15 @@
|
||||
import { api } from "../core/api.js";
|
||||
import { $ } from "../core/ui.js";
|
||||
import { $, esc, icon, toast } from "../core/ui.js";
|
||||
import { track } from "./jobs.js";
|
||||
|
||||
const COOKBOOK_MODELS = [
|
||||
const CURATED_MODELS = [
|
||||
{
|
||||
id: "qwen-coder-32b",
|
||||
name: "Qwen 2.5 Coder 32B",
|
||||
repo: "unsloth/Qwen2.5-Coder-32B-Instruct-GGUF",
|
||||
file: "Qwen2.5-Coder-32B-Instruct-Q4_K_M.gguf",
|
||||
desc: "Top-Tier lokales Coder-Modell. Braucht viel Speicher.",
|
||||
params_b: 32,
|
||||
quant: "Q4_K_M",
|
||||
ctx: 32768,
|
||||
alias: "coder"
|
||||
desc: "Top-Tier lokales Coder-Modell. Braucht ca. 24GB VRAM.",
|
||||
params_b: 32, quant: "Q4_K_M", ctx: 32768, alias: "coder"
|
||||
},
|
||||
{
|
||||
id: "qwen-coder-7b",
|
||||
@@ -19,10 +17,7 @@ const COOKBOOK_MODELS = [
|
||||
repo: "unsloth/Qwen2.5-Coder-7B-Instruct-GGUF",
|
||||
file: "Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf",
|
||||
desc: "Schneller Coder. Perfekte Balance aus Speed und Qualität.",
|
||||
params_b: 7,
|
||||
quant: "Q4_K_M",
|
||||
ctx: 32768,
|
||||
alias: "coder-fast"
|
||||
params_b: 7, quant: "Q4_K_M", ctx: 32768, alias: "coder-fast"
|
||||
},
|
||||
{
|
||||
id: "llama3-vision-11b",
|
||||
@@ -30,10 +25,7 @@ const COOKBOOK_MODELS = [
|
||||
repo: "unsloth/Llama-3.2-11B-Vision-Instruct-GGUF",
|
||||
file: "Llama-3.2-11B-Vision-Instruct-Q4_K_M.gguf",
|
||||
desc: "Modell für Bilderkennung und multimodale Tasks.",
|
||||
params_b: 11,
|
||||
quant: "Q4_K_M",
|
||||
ctx: 8192,
|
||||
alias: "vision"
|
||||
params_b: 11, quant: "Q4_K_M", ctx: 8192, alias: "vision"
|
||||
},
|
||||
{
|
||||
id: "qwen-general-7b",
|
||||
@@ -41,185 +33,245 @@ const COOKBOOK_MODELS = [
|
||||
repo: "unsloth/Qwen2.5-7B-Instruct-GGUF",
|
||||
file: "Qwen2.5-7B-Instruct-Q4_K_M.gguf",
|
||||
desc: "Hervorragendes Generalist/Scout Modell.",
|
||||
params_b: 7,
|
||||
quant: "Q4_K_M",
|
||||
ctx: 8192,
|
||||
alias: "scout"
|
||||
params_b: 7, quant: "Q4_K_M", ctx: 8192, alias: "scout"
|
||||
}
|
||||
];
|
||||
|
||||
// Odysseus Hardware Fit Logic
|
||||
function estimateMemoryGB(params_b, quant, ctx) {
|
||||
// Q4_K_M is roughly 0.6 bytes per param
|
||||
const bpp = 0.6;
|
||||
const weights = params_b * bpp;
|
||||
// Context roughly: 1GB per 8K for 7B
|
||||
const context = (ctx / 8192) * (params_b / 7) * 0.8;
|
||||
return weights + context;
|
||||
}
|
||||
|
||||
function getFit(m, sys) {
|
||||
const req = estimateMemoryGB(m.params_b, m.quant, m.ctx);
|
||||
// Falls keine sys-Daten da sind (Backend mock), nimm Standardwerte an
|
||||
const vram_bytes = sys?.gpu?.vram?.total || 0;
|
||||
const vram = vram_bytes / (1024 ** 3);
|
||||
const ram_bytes = sys?.ram?.total || 0;
|
||||
const ram_used = sys?.ram?.used || 0;
|
||||
const ram = (ram_bytes) / (1024 ** 3);
|
||||
const ram = ram_bytes / (1024 ** 3);
|
||||
const freeRam = (ram_bytes - ram_used) / (1024 ** 3);
|
||||
|
||||
// Wenn gar keine echten Metriken kommen (Windows Dummy Backend), immer "Fits" für Demo
|
||||
if (vram === 0 && ram === 0) return { level: "perfect", class: "green", text: "Fits (Mock)", req };
|
||||
|
||||
if (vram > 0 && req <= vram) return { level: "perfect", class: "green", text: "Fits VRAM", req };
|
||||
if (req <= (vram + freeRam)) return { level: "good", class: "yellow", text: "RAM Offload", req };
|
||||
return { level: "too_tight", class: "red", text: "OOM (Zu groß)", req };
|
||||
if (vram === 0 && ram === 0) return { level: "perfect", class: "b-run", text: "Fits (Mock)", req };
|
||||
if (vram > 0 && req <= vram) return { level: "perfect", class: "b-run", text: "Fits VRAM", req };
|
||||
if (req <= (vram + freeRam)) return { level: "good", class: "b-load", text: "RAM Offload", req };
|
||||
return { level: "too_tight", class: "b-err", text: "OOM (Zu groß)", req };
|
||||
}
|
||||
|
||||
let lastSys = null;
|
||||
let dlPoll = null;
|
||||
let currentResults = [];
|
||||
|
||||
function mount() {
|
||||
render();
|
||||
}
|
||||
|
||||
function unmount() {
|
||||
if (dlPoll) clearInterval(dlPoll);
|
||||
}
|
||||
|
||||
function onSystem(sys) {
|
||||
lastSys = sys;
|
||||
renderGrid();
|
||||
}
|
||||
|
||||
export default { mount, unmount, onSystem };
|
||||
|
||||
function render() {
|
||||
const c = document.querySelector(".view[data-view='cookbook']");
|
||||
const c = $(".view[data-view='cookbook']");
|
||||
c.innerHTML = `
|
||||
<div class="card" style="padding-bottom:32px">
|
||||
<div style="max-width:600px; margin:0 auto; text-align:center;">
|
||||
<h2 style="margin-top:20px; margin-bottom:8px">HuggingFace Suche</h2>
|
||||
<p class="meta" style="margin-bottom:24px">Finde GGUF-Modelle direkt auf HuggingFace und lade sie in Mission Control herunter.</p>
|
||||
|
||||
<div style="display:flex; gap:12px; position:relative">
|
||||
<input id="cb-search" class="tokin" style="flex:1; padding:12px 18px; font-size:16px; border-radius:12px;" placeholder="Nach Modellen suchen (z.B. Llama 3)...">
|
||||
<button class="primary" id="cb-btn-search" style="border-radius:12px; padding:0 24px;">Suchen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-h" style="margin-top:24px"><h3 id="cb-section-title">Kuratierte Empfehlungen</h3></div>
|
||||
<div class="grid grid-3" id="cb-grid"></div>
|
||||
|
||||
<div class="card" style="margin-top: 24px;">
|
||||
<div class="card-h"><h3>Custom Download</h3></div>
|
||||
<div style="display:flex; gap:12px; margin-bottom:12px;">
|
||||
<input id="cb-repo" class="tokin" placeholder="HuggingFace Repo (z.B. unsloth/Qwen2.5-Coder-7B-Instruct-GGUF)" style="flex:2">
|
||||
<input id="cb-file" class="tokin" placeholder="Dateipfad (z.B. Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf)" style="flex:1">
|
||||
<button class="btn" id="cb-btn-dl">Herunterladen</button>
|
||||
</div>
|
||||
<div id="cb-dl-prog" style="display:none; color:var(--text-dim); font-size:12px; margin-bottom:12px;"></div>
|
||||
<div id="cb-register-box" style="display:none; padding-top:12px; border-top:1px solid var(--border);">
|
||||
<div style="margin-bottom:8px;">Modell heruntergeladen! Jetzt einpflegen:</div>
|
||||
<div style="display:flex; gap:12px;">
|
||||
<input id="cb-reg-alias" class="tokin" placeholder="Alias (z.B. coder)" style="flex:1">
|
||||
<input id="cb-reg-path" class="tokin" placeholder="Modell-Pfad" style="flex:2" readonly>
|
||||
<input id="cb-reg-ctx" class="tokin" placeholder="Ctx (z.B. 8192)" type="number" style="width:100px;">
|
||||
<button class="btn" id="cb-btn-reg">Einpflegen</button>
|
||||
<!-- Modal für HuggingFace Modell Details -->
|
||||
<div id="cb-modal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); z-index:100; align-items:center; justify-content:center;">
|
||||
<div class="card" style="width:100%; max-width:600px; max-height:80vh; overflow-y:auto; position:relative">
|
||||
<button id="cb-modal-close" class="ghost" style="position:absolute; top:12px; right:12px;">Schließen</button>
|
||||
<h2 id="cb-m-title" style="margin-top:0">Modell</h2>
|
||||
<p class="meta" id="cb-m-repo">repo/name</p>
|
||||
|
||||
<div style="margin-top:24px">
|
||||
<label>Wähle eine Quantisierung (GGUF-Datei)</label>
|
||||
<select id="cb-m-files" class="tokin" style="width:100%; margin-top:8px; padding:12px"></select>
|
||||
<div id="cb-m-loading" class="meta" style="margin-top:8px; display:none">Lade Dateien...</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px; display:flex; gap:12px">
|
||||
<div style="flex:1">
|
||||
<label>Alias (Rolle)</label>
|
||||
<input id="cb-m-alias" class="tokin" style="width:100%; margin-top:8px" placeholder="z.B. coder">
|
||||
</div>
|
||||
<div style="flex:1">
|
||||
<label>Context-Size</label>
|
||||
<input id="cb-m-ctx" class="tokin" style="width:100%; margin-top:8px" value="8192" type="number">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="primary" id="cb-m-download" style="width:100%; margin-top:24px; padding:12px">Herunterladen & Einpflegen</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById("cb-btn-dl").onclick = () => {
|
||||
startDownload(
|
||||
document.getElementById("cb-repo").value,
|
||||
document.getElementById("cb-file").value,
|
||||
"", 8192
|
||||
);
|
||||
};
|
||||
$("#cb-btn-search").addEventListener("click", doSearch);
|
||||
$("#cb-search").addEventListener("keydown", e => { if (e.key === "Enter") doSearch(); });
|
||||
$("#cb-modal-close").addEventListener("click", () => $("#cb-modal").style.display = "none");
|
||||
$("#cb-m-download").addEventListener("click", doDownload);
|
||||
|
||||
document.getElementById("cb-btn-reg").onclick = async () => {
|
||||
const alias = document.getElementById("cb-reg-alias").value;
|
||||
const path = document.getElementById("cb-reg-path").value;
|
||||
const ctx = document.getElementById("cb-reg-ctx").value;
|
||||
if (!alias || !path) return;
|
||||
try {
|
||||
await api("/api/models/register", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ alias, model_path: path, ctx_len: parseInt(ctx)||4096 })
|
||||
});
|
||||
alert("Erfolgreich eingepflegt!");
|
||||
document.getElementById("cb-register-box").style.display = "none";
|
||||
} catch(e) {
|
||||
alert("Fehler: " + e);
|
||||
}
|
||||
};
|
||||
|
||||
renderGrid();
|
||||
renderCurated();
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
const grid = document.getElementById("cb-grid");
|
||||
if (!grid) return;
|
||||
grid.innerHTML = COOKBOOK_MODELS.map(m => {
|
||||
const fit = getFit(m, lastSys);
|
||||
return `
|
||||
<div class="card" style="display:flex; flex-direction:column;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0">${m.name}</h3>
|
||||
<span style="font-size:11px; padding:2px 6px; border-radius:4px; background:${fit.class === 'green' ? 'rgba(0,255,0,0.1)' : fit.class === 'yellow' ? 'rgba(255,255,0,0.1)' : 'rgba(255,0,0,0.1)'}; color:${fit.class === 'green' ? '#4ade80' : fit.class === 'yellow' ? '#fde047' : '#f87171'};">
|
||||
${fit.text}
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size:12px; color:var(--text-dim); margin-top:8px; flex:1;">
|
||||
${m.desc}<br><br>
|
||||
<b>Größe:</b> ~${fit.req.toFixed(1)} GB (inkl. Context)<br>
|
||||
<b>Quant:</b> ${m.quant}
|
||||
</div>
|
||||
<button class="btn cb-btn-preset" data-id="${m.id}" style="margin-top:16px; width:100%;">Herunterladen</button>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
async function doSearch() {
|
||||
const q = $("#cb-search").value.trim();
|
||||
if (!q) return renderCurated();
|
||||
|
||||
document.querySelectorAll(".cb-btn-preset").forEach(b => {
|
||||
b.onclick = () => {
|
||||
const m = COOKBOOK_MODELS.find(x => x.id === b.dataset.id);
|
||||
if(m) {
|
||||
document.getElementById("cb-repo").value = m.repo;
|
||||
document.getElementById("cb-file").value = m.file;
|
||||
startDownload(m.repo, m.file, m.alias, m.ctx);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
const btn = $("#cb-btn-search");
|
||||
btn.disabled = true; btn.textContent = "Lade...";
|
||||
$("#cb-section-title").textContent = "Suchergebnisse für: " + esc(q);
|
||||
$("#cb-grid").innerHTML = `<div class="meta" style="grid-column:1/-1; text-align:center; padding:40px">Suche auf HuggingFace...</div>`;
|
||||
|
||||
async function startDownload(repo, file, autoAlias, autoCtx) {
|
||||
if(!repo || !file) return;
|
||||
document.getElementById("cb-dl-prog").style.display = "block";
|
||||
document.getElementById("cb-dl-prog").innerText = "Starte Download...";
|
||||
document.getElementById("cb-register-box").style.display = "none";
|
||||
|
||||
try {
|
||||
const res = await api("/api/models/download", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ repo_id: repo, filename: file })
|
||||
});
|
||||
if (res.job_id) {
|
||||
if(dlPoll) clearInterval(dlPoll);
|
||||
dlPoll = setInterval(() => checkProg(res.job_id, autoAlias, autoCtx), 2000);
|
||||
const url = \`https://huggingface.co/api/models?search=\${encodeURIComponent(q)}&filter=gguf&sort=downloads&direction=-1&limit=12\`;
|
||||
const r = await fetch(url);
|
||||
const data = await r.json();
|
||||
currentResults = data;
|
||||
renderResults(data);
|
||||
} catch (e) {
|
||||
$("#cb-grid").innerHTML = \`<div class="alert err" style="grid-column:1/-1">\${esc(e.message)}</div>\`;
|
||||
}
|
||||
btn.disabled = false; btn.textContent = "Suchen";
|
||||
}
|
||||
|
||||
function renderResults(results) {
|
||||
const grid = $("#cb-grid");
|
||||
if (!results || results.length === 0) {
|
||||
grid.innerHTML = `<div class="meta" style="grid-column:1/-1; text-align:center; padding:40px">Keine GGUF-Modelle gefunden.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = results.map((m, i) => \`
|
||||
<div class="card" style="display:flex; flex-direction:column; cursor:pointer" onclick="window.openModelModal(\${i})">
|
||||
<h3 style="margin:0; font-size:16px; word-break:break-all">\${esc(m.id.split('/').pop())}</h3>
|
||||
<div class="meta" style="font-size:12px; margin-top:4px">\${esc(m.author)}</div>
|
||||
<div style="flex:1; margin-top:16px;"></div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:16px">
|
||||
<span class="badge b-idle" style="font-size:11px">GGUF</span>
|
||||
<span class="meta" style="font-size:12px">⬇ \${(m.downloads||0).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
\`).join("");
|
||||
}
|
||||
|
||||
// Global hook for inline onclick
|
||||
window.openModelModal = async (index) => {
|
||||
const m = currentResults[index];
|
||||
if (!m) return;
|
||||
$("#cb-modal").style.display = "flex";
|
||||
$("#cb-m-title").textContent = m.id.split('/').pop();
|
||||
$("#cb-m-repo").textContent = m.id;
|
||||
$("#cb-m-files").innerHTML = "";
|
||||
$("#cb-m-alias").value = m.id.split('/').pop().toLowerCase().replace(/[^a-z0-9]/g, "-");
|
||||
|
||||
$("#cb-m-files").style.display = "none";
|
||||
$("#cb-m-loading").style.display = "block";
|
||||
|
||||
try {
|
||||
const url = \`https://huggingface.co/api/models/\${m.id}/tree/main\`;
|
||||
const r = await fetch(url);
|
||||
const tree = await r.json();
|
||||
const files = tree.filter(f => f.path.endsWith('.gguf')).map(f => f.path);
|
||||
|
||||
$("#cb-m-loading").style.display = "none";
|
||||
$("#cb-m-files").style.display = "block";
|
||||
|
||||
if (files.length === 0) {
|
||||
$("#cb-m-files").innerHTML = "<option value=''>Keine GGUF-Dateien im Hauptverzeichnis gefunden.</option>";
|
||||
$("#cb-m-download").disabled = true;
|
||||
} else {
|
||||
$("#cb-m-files").innerHTML = files.map(f => \`<option value="\${esc(f)}">\${esc(f)}</option>\`).join("");
|
||||
$("#cb-m-download").disabled = false;
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById("cb-dl-prog").innerText = "Fehler: " + e;
|
||||
$("#cb-m-loading").textContent = "Fehler beim Laden der Dateien.";
|
||||
}
|
||||
};
|
||||
|
||||
async function doDownload() {
|
||||
const repo = $("#cb-m-repo").textContent;
|
||||
const file = $("#cb-m-files").value;
|
||||
const alias = $("#cb-m-alias").value.trim();
|
||||
const ctx = parseInt($("#cb-m-ctx").value) || 8192;
|
||||
|
||||
if (!repo || !file || !alias) return toast("Bitte alle Felder ausfüllen.", true);
|
||||
|
||||
$("#cb-m-download").disabled = true;
|
||||
$("#cb-m-download").textContent = "Starte...";
|
||||
|
||||
try {
|
||||
const res = await api("/api/download", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ repo, file })
|
||||
});
|
||||
|
||||
// Register directly, jobengine will download it, MC will pick it up when done
|
||||
await api("/api/register", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ alias, model_path: res.expected_path, ctx })
|
||||
});
|
||||
|
||||
toast("Download gestartet! Siehe Aktivitäten.");
|
||||
$("#cb-modal").style.display = "none";
|
||||
track(res.job_id); // open in jobs
|
||||
// Switch to activity tab
|
||||
document.querySelector(".nav-item[data-view='activity']").click();
|
||||
|
||||
} catch (e) {
|
||||
toast("Fehler: " + e.message, true);
|
||||
}
|
||||
|
||||
$("#cb-m-download").disabled = false;
|
||||
$("#cb-m-download").textContent = "Herunterladen & Einpflegen";
|
||||
}
|
||||
|
||||
async function checkProg(jobId, autoAlias, autoCtx) {
|
||||
try {
|
||||
const res = await api("/api/jobs");
|
||||
const j = res.find(x => x.id === jobId);
|
||||
if (!j) {
|
||||
clearInterval(dlPoll);
|
||||
document.getElementById("cb-dl-prog").innerText = "Job verschwunden.";
|
||||
return;
|
||||
}
|
||||
document.getElementById("cb-dl-prog").innerText = j.status + (j.error ? (" - " + j.error) : "");
|
||||
if (j.status === "done") {
|
||||
clearInterval(dlPoll);
|
||||
document.getElementById("cb-register-box").style.display = "block";
|
||||
document.getElementById("cb-reg-path").value = j.result || "";
|
||||
if(autoAlias) document.getElementById("cb-reg-alias").value = autoAlias;
|
||||
if(autoCtx) document.getElementById("cb-reg-ctx").value = autoCtx;
|
||||
} else if (j.status === "failed") {
|
||||
clearInterval(dlPoll);
|
||||
}
|
||||
} catch(e) {}
|
||||
function renderCurated() {
|
||||
$("#cb-section-title").textContent = "Kuratierte Empfehlungen";
|
||||
const grid = $("#cb-grid");
|
||||
if (!grid) return;
|
||||
grid.innerHTML = CURATED_MODELS.map((m, i) => {
|
||||
const fit = getFit(m, lastSys);
|
||||
return \`
|
||||
<div class="card" style="display:flex; flex-direction:column; cursor:pointer" onclick="window.openCuratedModal(\${i})">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0; font-size:16px">\${esc(m.name)}</h3>
|
||||
<span class="badge \${fit.class}">\${fit.text}</span>
|
||||
</div>
|
||||
<div style="font-size:13px; color:var(--mut); margin-top:12px; flex:1; line-height:1.5;">
|
||||
\${m.desc}
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; margin-top:16px; font-size:12px" class="meta">
|
||||
<span>~\${fit.req.toFixed(1)} GB RAM</span>
|
||||
<span>\${m.quant}</span>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
window.openCuratedModal = (index) => {
|
||||
const m = CURATED_MODELS[index];
|
||||
if (!m) return;
|
||||
$("#cb-modal").style.display = "flex";
|
||||
$("#cb-m-title").textContent = m.name;
|
||||
$("#cb-m-repo").textContent = m.repo;
|
||||
$("#cb-m-files").innerHTML = \`<option value="\${esc(m.file)}">\${esc(m.file)}</option>\`;
|
||||
$("#cb-m-files").style.display = "block";
|
||||
$("#cb-m-loading").style.display = "none";
|
||||
$("#cb-m-alias").value = m.alias;
|
||||
$("#cb-m-ctx").value = m.ctx;
|
||||
$("#cb-m-download").disabled = false;
|
||||
};
|
||||
|
||||
function onSystem(sys) {
|
||||
lastSys = sys;
|
||||
if (!$("#cb-search").value.trim()) renderCurated();
|
||||
}
|
||||
|
||||
function unmount() {}
|
||||
|
||||
export default { id: "cookbook", mount, unmount, onSystem };
|
||||
|
||||
Reference in New Issue
Block a user