diff --git a/static/js/cookbook-hwfit.js b/static/js/cookbook-hwfit.js
index 7fab5570c..25a65779c 100644
--- a/static/js/cookbook-hwfit.js
+++ b/static/js/cookbook-hwfit.js
@@ -468,7 +468,7 @@ function _hwfitShowError(list, host, detail) {
if (rb) rb.addEventListener('click', () => { _resetGpuToggleState(); _hwfitFetch(true); });
}
-// Client-side "Engine" filter (llama.cpp / vLLM / SGLang / Ollama). Empty =
+// Client-side "Engine" filter (llama.cpp / vLLM / SGLang / Ollama / Diffusers). Empty =
// show all. Uses the same _detectBackend() the serve commands use, so what you
// filter to is exactly what would be launched. Pure view filter — no refetch
// needed. Ollama rows are merged into the main list (see _ensureOllamaLib +
@@ -1804,6 +1804,83 @@ export function _expandModelRow(row, modelData) {
}
+const _HWFIT_ENGINE_GLYPHS = {
+ '': '',
+ vllm: '',
+ sglang: '',
+ llamacpp: '',
+ ollama: '',
+ diffusers: '',
+};
+
+function _hwfitEngineGlyph(value) {
+ return _HWFIT_ENGINE_GLYPHS[value] || _HWFIT_ENGINE_GLYPHS[''];
+}
+
+function _bindHwfitEnginePicker(engine) {
+ const wrap = engine?.closest('.hwfit-engine-wrap');
+ const btn = wrap?.querySelector('[data-hwfit-engine-btn]');
+ const menu = wrap?.querySelector('[data-hwfit-engine-menu]');
+ const icon = wrap?.querySelector('[data-hwfit-engine-icon]');
+ const label = wrap?.querySelector('[data-hwfit-engine-label]');
+ if (!engine || !wrap || !btn || !menu || wrap.dataset.enginePickerBound) return;
+ wrap.dataset.enginePickerBound = '1';
+
+ const setOpen = (open) => {
+ menu.hidden = !open;
+ btn.setAttribute('aria-expanded', open ? 'true' : 'false');
+ };
+ const currentLabel = () => {
+ const opt = Array.from(engine.options).find((o) => o.value === engine.value);
+ return opt?.textContent || 'Engine';
+ };
+ const syncButton = () => {
+ if (label) label.textContent = currentLabel();
+ if (icon) icon.innerHTML = _hwfitEngineGlyph(engine.value);
+ menu.querySelectorAll('[data-hwfit-engine-value]').forEach((item) => {
+ const active = item.dataset.hwfitEngineValue === engine.value;
+ item.classList.toggle('active', active);
+ item.setAttribute('aria-selected', active ? 'true' : 'false');
+ });
+ };
+ const renderMenu = () => {
+ menu.innerHTML = Array.from(engine.options).map((opt) => (
+ `'
+ )).join('');
+ menu.querySelectorAll('[data-hwfit-engine-value]').forEach((item) => {
+ item.addEventListener('click', (ev) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ const next = item.dataset.hwfitEngineValue || '';
+ if (engine.value !== next) {
+ engine.value = next;
+ engine.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+ syncButton();
+ setOpen(false);
+ });
+ });
+ syncButton();
+ };
+
+ btn.addEventListener('click', (ev) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ setOpen(menu.hidden);
+ });
+ engine.addEventListener('change', syncButton);
+ document.addEventListener('click', (ev) => {
+ if (!wrap.contains(ev.target)) setOpen(false);
+ });
+ document.addEventListener('keydown', (ev) => {
+ if (ev.key === 'Escape') setOpen(false);
+ });
+ renderMenu();
+}
+
export function _hwfitInit() {
const uc = document.getElementById('hwfit-usecase');
const sort = document.getElementById('hwfit-sort');
@@ -1819,6 +1896,7 @@ export function _hwfitInit() {
// Engine filter is a pure client-side view filter over the already-fetched
// list (HF + Ollama merged), so just re-render from cache.
const engine = document.getElementById('hwfit-engine');
+ if (engine) _bindHwfitEnginePicker(engine);
if (engine) engine.addEventListener('change', () => {
const list = document.getElementById('hwfit-list');
if (list && _hwfitCache && Array.isArray(_hwfitCache.models)) {
diff --git a/static/js/cookbook.js b/static/js/cookbook.js
index 6451f4bc0..972f38014 100644
--- a/static/js/cookbook.js
+++ b/static/js/cookbook.js
@@ -2692,13 +2692,20 @@ function _renderRecipes() {
// levers (Engine / Quant / Context) live to the right.
html += '';
html += '';
- html += '';
// Quant (Q4/Q8/…). Default is "All" so the list shows the best-scoring
diff --git a/static/js/cookbookServe.js b/static/js/cookbookServe.js
index 511c49564..981e70f4c 100644
--- a/static/js/cookbookServe.js
+++ b/static/js/cookbookServe.js
@@ -1390,7 +1390,7 @@ function _rerenderCachedModels() {
// Row 2b: Diffusers settings
const diffDtypeOpts = ['bfloat16','float16','float32'].map(d => ``).join('');
const deviceMapOpts = ['balanced','auto','sequential'].map(d => ``).join('');
- panelHtml += `
`;
+ panelHtml += `
`;
panelHtml += ``;
panelHtml += ``;
panelHtml += ``;
@@ -1495,11 +1495,11 @@ function _rerenderCachedModels() {
panelHtml += ``;
panelHtml += `
`;
// Row 3b: Checkboxes (diffusers)
- panelHtml += `
`;
+ panelHtml += `
`;
panelHtml += ``;
panelHtml += ``;
panelHtml += ``;
- panelHtml += `
`;
+ panelHtml += `
`;
panelHtml += ``;
panelHtml += `
`;
// Model-specific optimizations. The checks row always renders for the
diff --git a/static/style.css b/static/style.css
index 731961888..99523d7db 100644
--- a/static/style.css
+++ b/static/style.css
@@ -20479,7 +20479,7 @@ body.gallery-selecting .gallery-dl-btn,
width: 100%;
}
.hwfit-serve-cmd-details {
- margin: 6px 0 0;
+ margin: 4px 0 0;
}
.hwfit-serve-cmd-summary {
display: flex;
@@ -22201,6 +22201,7 @@ body.gallery-selecting .gallery-dl-btn,
.hwfit-toolbar .hwfit-usecase { min-width: 70px; flex-shrink: 0; }
.hwfit-toolbar .hwfit-quant { min-width: 0; width: 78px; flex-shrink: 0; font-size: 10px; }
.hwfit-toolbar .hwfit-engine { min-width: 0; width: 86px; flex-shrink: 0; font-size: 10px; }
+.hwfit-toolbar .hwfit-engine-btn { min-width: 0; width: 86px; flex-shrink: 0; font-size: 10px; }
.hwfit-toolbar .hwfit-search { flex: 1; min-width: 80px; }
/* Lower-opacity "Search models..." placeholder so it reads as a hint, not
a label — matches the muted form-field feel of the inline filters. */
@@ -22271,6 +22272,83 @@ body.gallery-selecting .gallery-dl-btn,
display: inline-flex;
align-items: center;
}
+.hwfit-engine-btn {
+ height: 28px;
+ padding: 0 30px 0 7px;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ color: var(--fg);
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ font: inherit;
+ cursor: pointer;
+ text-align: left;
+}
+.hwfit-engine-btn-icon,
+.hwfit-engine-item-icon {
+ width: 14px;
+ height: 14px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--accent, var(--red));
+ flex-shrink: 0;
+}
+.hwfit-engine-btn-label {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.hwfit-engine-caret {
+ position: absolute;
+ right: 6px;
+ opacity: 0.55;
+ pointer-events: none;
+}
+.hwfit-engine-menu {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ right: 0;
+ z-index: 120;
+ padding: 4px;
+ background: var(--panel, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ box-shadow: 0 6px 20px rgba(0,0,0,0.22);
+}
+.hwfit-engine-item {
+ width: 100%;
+ min-height: 26px;
+ padding: 5px 7px;
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ color: var(--fg);
+ background: transparent;
+ border: 0;
+ border-radius: 5px;
+ font: inherit;
+ font-size: 11px;
+ text-align: left;
+ cursor: pointer;
+ box-sizing: border-box;
+}
+.hwfit-engine-item:hover,
+.hwfit-engine-item.active {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.hwfit-engine-item-label {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
.hwfit-quant-wrap .hwfit-quant,
.hwfit-engine-wrap .hwfit-engine {
/* Make room for the ? on the right edge, in addition to the native chevron. */
@@ -24056,6 +24134,16 @@ details.hwfit-serve-advanced > .hwfit-llama-monitor-row,
details.hwfit-serve-advanced > .hwfit-llama-checks-row {
margin-top: -18px !important;
}
+details.hwfit-serve-advanced > .hwfit-diff-settings-row {
+ row-gap: 2px;
+}
+details.hwfit-serve-advanced > .hwfit-diff-checks-row,
+details.hwfit-serve-advanced > .hwfit-diff-harmonize-row {
+ margin-top: -10px !important;
+}
+details.hwfit-serve-advanced > .hwfit-diff-checks-row {
+ row-gap: 2px;
+}
details.hwfit-serve-advanced > .hwfit-serve-row.hwfit-vram-monitor {
gap: 4px !important;
grid-template-columns: max-content minmax(0, 1fr);