release: v0.2.37 — zero-argument startup, first-run wizard, real download progress
Startup no longer needs arguments: the config is created on first launch, a busy port is swapped for a free one, the console is open by default, and the browser lands on a setup wizard that downloads a ready-made .cmf model and wires it into routing (managed server + routing.default) when you press Finish. Download progress was reported from curl's own meter, which prints a full 0→100% bar for the -L redirect hop before the transfer starts — so every job showed 100% and then hung. Progress now comes from bytes on disk against the size from the HuggingFace tree (or a HEAD request). The UI and the gateway also disagreed about the output file name, so a finished download never marked its card as downloaded; the gateway now returns the exact stem and the UI sends it back verbatim for download, progress and delete. Featured models are read live from the infosave HF account in both the wizard and Import, and all 432 UI strings now exist in every one of the 7 languages.
This commit is contained in:
Vendored
+74
-42
@@ -5,6 +5,13 @@
|
||||
import { h, mount, toast, opt } from "../ui.js";
|
||||
import { t } from "../i18n.js";
|
||||
import { api } from "../api.js";
|
||||
import {
|
||||
loadFeatured,
|
||||
featuredCard,
|
||||
paintFeaturedHosts,
|
||||
startFeaturedDownload,
|
||||
jobStatusText,
|
||||
} from "./featured.js";
|
||||
|
||||
const QUANTS = [
|
||||
["Q8_2F", "import.q.q8_2f"],
|
||||
@@ -20,12 +27,6 @@ const QUANTS = [
|
||||
["F32", "import.q.f32"],
|
||||
];
|
||||
|
||||
const FEATURED_REPOS = [
|
||||
{ id: "infosave/Bonsai-1.7Bcmf", label: "Bonsai 1.7B CMF", badge: "1.7B · Q1", quant: "Q1" },
|
||||
{ id: "infosave/Bonsai-8B_2bit_cmf", label: "Bonsai 8B 2-bit CMF", badge: "8B · Q1T", quant: "Q1T" },
|
||||
{ id: "infosave/Bonsai-27Bcmf", label: "Bonsai 27B CMF", badge: "27B · Q1", quant: "Q1" },
|
||||
];
|
||||
|
||||
|
||||
function field(label, control, hint) {
|
||||
return h("label", { class: "field" }, h("span", {}, label), control,
|
||||
@@ -53,6 +54,10 @@ export function renderImport() {
|
||||
let selected = null;
|
||||
let lastModels = []; // cache of the last search, so re-highlighting needs no refetch
|
||||
let jobsTimer = null;
|
||||
let featured = []; // ready-made .cmf models from the infosave HF account
|
||||
let featuredHosts = new Map(); // job id → live progress slot inside a featured card
|
||||
let lastJobs = [];
|
||||
let jobStates = new Map(); // job id → last seen state, to spot completions
|
||||
|
||||
const searchIn = h("input", {
|
||||
class: "hf-search",
|
||||
@@ -254,6 +259,12 @@ export function renderImport() {
|
||||
}
|
||||
// Load + render all jobs. Runs on mount (before `root` is attached — so no
|
||||
// isConnected guard here) and on each poll tick.
|
||||
async function cancelJob(j) {
|
||||
await api.cancelImport(j.id);
|
||||
toast(t("import.cancelling"), "warn");
|
||||
await refreshJobs();
|
||||
}
|
||||
|
||||
async function refreshJobs() {
|
||||
let jobs = [];
|
||||
try {
|
||||
@@ -262,7 +273,14 @@ export function renderImport() {
|
||||
return;
|
||||
}
|
||||
jobs = jobs.filter((j) => j.state !== "cancelled"); // hide user-cancelled jobs
|
||||
lastJobs = jobs;
|
||||
renderJobs(jobs);
|
||||
// A settled download changes what the featured cards offer (✓ + delete), so
|
||||
// re-read the list on that transition — not on every poll tick.
|
||||
const settled = jobs.some((j) => jobStates.get(j.id) === "running" && j.state !== "running");
|
||||
jobStates = new Map(jobs.map((j) => [j.id, j.state]));
|
||||
if (settled) await renderFeatured();
|
||||
else paintFeaturedHosts(featuredHosts, jobs, cancelJob);
|
||||
if (jobs.some((j) => j.state === "running")) ensurePolling();
|
||||
else stopPolling();
|
||||
}
|
||||
@@ -289,7 +307,7 @@ export function renderImport() {
|
||||
(running && pct != null ? " · " + pct + "%" : "") +
|
||||
(j.size_bytes ? " · " + fmtBytes(j.size_bytes) : "")
|
||||
);
|
||||
const phase = h("div", { class: "muted small" }, j.phase || "");
|
||||
const phase = h("div", { class: "muted small" }, jobStatusText(j));
|
||||
const logEl = h("pre", { class: "conv-log" }, (j.log || []).slice(-10).join("\n"));
|
||||
|
||||
const actions = [];
|
||||
@@ -367,46 +385,59 @@ export function renderImport() {
|
||||
);
|
||||
}
|
||||
|
||||
function featuredCard(item) {
|
||||
const card = h(
|
||||
"div",
|
||||
{ class: "hf-card featured-card" },
|
||||
h("div", { class: "row", style: "align-items:center; justify-content:space-between;" },
|
||||
h("div", {},
|
||||
h("div", { class: "hf-id" }, item.label),
|
||||
h("div", { class: "hf-meta" },
|
||||
h("span", { class: "badge" }, item.badge),
|
||||
h("span", { class: "muted" }, item.id)
|
||||
)
|
||||
),
|
||||
h("button", { class: "btn primary sm" }, "⬇ " + t("import.directDownload"))
|
||||
)
|
||||
);
|
||||
card.querySelector("button").addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await api.startImport({
|
||||
repo: item.id,
|
||||
quant: item.quant || "READY_CMF",
|
||||
name: item.id.split("/").pop().toLowerCase(),
|
||||
});
|
||||
toast(t("import.started"), "ok");
|
||||
refreshJobs();
|
||||
jobsHost.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
} catch (err) {
|
||||
toast(err.message, "err");
|
||||
}
|
||||
});
|
||||
return card;
|
||||
}
|
||||
|
||||
// ── featured: the infosave account, read live so new releases just show up ──
|
||||
const featuredGrid = h("div", { class: "hf-grid" }, h("div", { class: "muted pad" }, t("import.searching")));
|
||||
const reloadBtn = h("button", { class: "btn ghost sm" }, "↻ " + t("common.refresh"));
|
||||
const featuredHost = h(
|
||||
"div",
|
||||
{ class: "card featured-section" },
|
||||
h("div", { class: "card-title" }, "⚡ " + t("import.featuredTitle")),
|
||||
h("div", { class: "hf-grid" }, ...FEATURED_REPOS.map(featuredCard))
|
||||
h("div", { class: "card-head", style: "display:flex; align-items:center; gap:10px" },
|
||||
h("div", { class: "card-title", style: "margin:0" }, "⚡ " + t("import.featuredTitle")),
|
||||
h("div", { style: "flex:1" }),
|
||||
reloadBtn),
|
||||
h("div", { class: "muted small", style: "margin:2px 0 10px" }, t("import.featuredSub")),
|
||||
featuredGrid
|
||||
);
|
||||
|
||||
async function startFeatured(m, v) {
|
||||
try {
|
||||
const variant = await startFeaturedDownload(m, v);
|
||||
toast(t("import.started") + (variant ? ` · ${variant}` : ""), "ok");
|
||||
await refreshJobs();
|
||||
jobsHost.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
} catch (e) { toast(e.message, "err"); }
|
||||
}
|
||||
|
||||
async function deleteFeatured(m, v) {
|
||||
const name = (v && v.local_name) || m.local_name || m.id;
|
||||
if (!window.confirm(t("onboarding.confirmDelete", { id: name }))) return;
|
||||
try {
|
||||
const r = await api.deleteSetupFile(m.id, v && v.name, (v && v.local_name) || m.local_name);
|
||||
toast(t("import.deleted"), "ok");
|
||||
if (r && r.unregistered && r.unregistered.length) {
|
||||
toast(t("onboarding.unregistered", { ids: r.unregistered.join(", ") }), "ok");
|
||||
}
|
||||
await renderFeatured();
|
||||
await refreshJobs();
|
||||
} catch (e) { toast(e.message, "err"); }
|
||||
}
|
||||
|
||||
async function renderFeatured(reload = true) {
|
||||
if (reload) featured = await loadFeatured();
|
||||
featuredHosts = new Map();
|
||||
const ctx = { hosts: featuredHosts, onDownload: startFeatured, onDelete: deleteFeatured };
|
||||
mount(featuredGrid, ...(featured.length
|
||||
? featured.map((m) => featuredCard(m, ctx))
|
||||
: [h("div", { class: "muted pad" }, t("import.noResults"))]));
|
||||
paintFeaturedHosts(featuredHosts, lastJobs, cancelJob);
|
||||
}
|
||||
reloadBtn.addEventListener("click", async () => {
|
||||
reloadBtn.disabled = true;
|
||||
mount(featuredGrid, h("div", { class: "muted pad" }, t("import.searching")));
|
||||
await renderFeatured();
|
||||
reloadBtn.disabled = false;
|
||||
});
|
||||
|
||||
const root = h(
|
||||
"div",
|
||||
{},
|
||||
@@ -419,6 +450,7 @@ export function renderImport() {
|
||||
formHost
|
||||
);
|
||||
doSearch(); // initial trending list (mounts into `results`)
|
||||
renderFeatured(); // ready-made .cmf models, fetched live
|
||||
refreshJobs(); // resume: show any running/recent conversions immediately
|
||||
return root;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user