mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-09 06:58:41 -04:00
Merge verified Odysseus fixes
This commit is contained in:
+262
-91
@@ -504,6 +504,14 @@ export function _detectBackend(model) {
|
||||
const isRocm = sysBackend === 'rocm';
|
||||
const isAppleSilicon = ['metal', 'mps', 'apple'].includes(sysBackend);
|
||||
const _nm = `${model.repo_id || ''} ${model.path || ''} ${model.name || ''}`.toLowerCase();
|
||||
const isImageModel = !!(model.is_image_gen || model.is_diffusion || model._tag === 'image');
|
||||
// Image gen models → diffusers
|
||||
if (isImageModel) {
|
||||
if (/\bmlx\b|mlx-|_mlx|mlx-community\//i.test(_nm) || q.startsWith('MLX') || model.mlx_only) {
|
||||
return { backend: 'mlx_image', label: 'MLX Image' };
|
||||
}
|
||||
return { backend: 'diffusers', label: 'Diffusers' };
|
||||
}
|
||||
if (/\bmlx\b|mlx-|_mlx/i.test(_nm) || q.startsWith('MLX')) {
|
||||
return { backend: 'mlx', label: 'MLX' };
|
||||
}
|
||||
@@ -512,11 +520,6 @@ export function _detectBackend(model) {
|
||||
&& model.gguf_files.some(f => f && typeof f.rel_path === 'string' && /\.gguf$/i.test(f.rel_path));
|
||||
const isGgufLike = model.is_gguf || hasGgufFile || /^Q[2-8]/.test(q) || /^IQ/.test(q) || q === 'GGUF' || _nm.includes('gguf');
|
||||
|
||||
// Image gen models → diffusers
|
||||
if (model.is_image_gen || model.is_diffusion || model._tag === 'image') {
|
||||
return { backend: 'diffusers', label: 'Diffusers' };
|
||||
}
|
||||
|
||||
// AWQ / GPTQ / FP8 are safetensors GPU-serving formats. Never route them
|
||||
// through llama.cpp/Ollama just because the host is Mac/Windows; those engines
|
||||
// need GGUF. The UI will warn/block on Metal where vLLM/SGLang aren't viable.
|
||||
@@ -558,6 +561,18 @@ export function _shellQuote(value) {
|
||||
return "'" + String(value ?? '').replace(/'/g, "'\\''") + "'";
|
||||
}
|
||||
|
||||
function _listField(value) {
|
||||
return String(value || '')
|
||||
.split(/[\n,]+/)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function _numField(value) {
|
||||
const s = String(value || '').trim();
|
||||
return /^-?\d+(?:\.\d+)?$/.test(s) ? s : '';
|
||||
}
|
||||
|
||||
export function _psQuote(value) {
|
||||
return "'" + String(value ?? '').replace(/'/g, "''") + "'";
|
||||
}
|
||||
@@ -709,6 +724,10 @@ export function _buildServeCmd(f, modelName, backend) {
|
||||
const _kv = (f.vllm_kv_cache_dtype ?? '').toString().trim();
|
||||
if (_kv === 'fp8') cmd += ' --kv-cache-dtype fp8';
|
||||
if (f.max_seqs && f.max_seqs.toString().trim()) cmd += ` --max-num-seqs ${f.max_seqs.toString().trim()}`;
|
||||
const _vllmLoraModules = _listField(f.vllm_lora_modules);
|
||||
if (_vllmLoraModules.length) {
|
||||
cmd += ` --enable-lora --lora-modules ${_vllmLoraModules.map(_shellQuote).join(' ')}`;
|
||||
}
|
||||
if (f.enforce_eager) cmd += ' --enforce-eager';
|
||||
if (f.trust_remote) cmd += ' --trust-remote-code';
|
||||
if (f.prefix_cache) cmd += ' --enable-prefix-caching';
|
||||
@@ -917,7 +936,7 @@ export function _buildServeCmd(f, modelName, backend) {
|
||||
// Trailing GGUF_FILE is optional; helper picks the first match if empty.
|
||||
cmd = `docker exec ollama-test ollama-import ${modelName} ${_name} ${_ctx}${_file ? ' ' + _file : ''}`;
|
||||
} else if (!modelName.includes('/') && modelName) {
|
||||
// Already-pulled Ollama tag (e.g. `qwen2.5:7b`). On kierkegaard the
|
||||
// Already-pulled Ollama tag (e.g. `qwen2.5:7b`). On remote hosts the
|
||||
// runtime is the ROCm Ollama sidecar; this quick command verifies the
|
||||
// tag exists, then the backend auto-registers http://host.docker.internal:11434/v1.
|
||||
cmd = `docker exec ollama-rocm ollama show ${modelName}`;
|
||||
@@ -930,22 +949,54 @@ export function _buildServeCmd(f, modelName, backend) {
|
||||
const gpuStr = f.gpus?.trim();
|
||||
cmd += _gpuEnvPrefix(gpuStr);
|
||||
const diffusersPy = _isWindows() ? 'python' : _py3Bin;
|
||||
cmd += `${diffusersPy} scripts/diffusion_server.py --model ${modelName} --port ${f.port || '8100'}`;
|
||||
const diffHost = f.host ? '0.0.0.0' : '127.0.0.1';
|
||||
cmd += `${diffusersPy} scripts/diffusion_server.py --model ${modelName} --host ${diffHost} --port ${f.port || '8100'}`;
|
||||
if (f.host) {
|
||||
const allowedHost = String(f.host || '').split('@').pop().split(':')[0].trim();
|
||||
if (allowedHost) cmd += ` --allowed-host ${allowedHost}`;
|
||||
}
|
||||
if (f.diff_dtype && f.diff_dtype !== 'bfloat16') cmd += ` --dtype ${f.diff_dtype}`;
|
||||
if (f.diff_device_map && f.diff_device_map !== 'balanced') cmd += ` --device-map ${f.diff_device_map}`;
|
||||
if (f.diff_steps) cmd += ` --steps ${f.diff_steps}`;
|
||||
if (f.diff_guidance_scale) cmd += ` --guidance-scale ${_numField(f.diff_guidance_scale) || f.diff_guidance_scale}`;
|
||||
if (String(f.diff_negative_prompt || '').trim()) cmd += ` --negative-prompt ${_shellQuote(String(f.diff_negative_prompt || '').trim())}`;
|
||||
if (f.diff_width) cmd += ` --width ${f.diff_width}`;
|
||||
if (f.diff_height) cmd += ` --height ${f.diff_height}`;
|
||||
const _diffLoras = _listField(f.diff_lora);
|
||||
if (_diffLoras.length) cmd += ` --lora ${_shellQuote(_diffLoras.join(','))}`;
|
||||
const _diffLoraScale = _numField(f.diff_lora_scale);
|
||||
if (_diffLoraScale) cmd += ` --lora-scale ${_diffLoraScale}`;
|
||||
if (f.diff_offload) cmd += ' --cpu-offload';
|
||||
if (f.diff_attention_slicing) cmd += ' --attention-slicing';
|
||||
if (f.diff_vae_slicing) cmd += ' --vae-slicing';
|
||||
if (f.diff_harmonize_gpu) cmd += ` --harmonize-gpu ${f.diff_harmonize_gpu}`;
|
||||
} else if (backend === 'mlx_image') {
|
||||
const mlxPy = _isWindows() ? 'python' : _py3Bin;
|
||||
const mlxHost = f.host ? '0.0.0.0' : '127.0.0.1';
|
||||
cmd += `${mlxPy} scripts/mlx_image_server.py --model ${_shellQuote(modelName)} --host ${mlxHost} --port ${f.port || '8100'}`;
|
||||
if (f.diff_steps) cmd += ` --steps ${f.diff_steps}`;
|
||||
if (f.diff_width) cmd += ` --width ${f.diff_width}`;
|
||||
if (f.diff_height) cmd += ` --height ${f.diff_height}`;
|
||||
const _mlxBaseModel = String(f.mlx_base_model || '').trim();
|
||||
if (_mlxBaseModel) cmd += ` --base-model ${_shellQuote(_mlxBaseModel)}`;
|
||||
const _mlxLoraStyle = String(f.mlx_lora_style || '').trim();
|
||||
if (_mlxLoraStyle) cmd += ` --lora-style ${_shellQuote(_mlxLoraStyle)}`;
|
||||
const _mlxLoraPaths = _listField(f.mlx_lora_paths);
|
||||
if (_mlxLoraPaths.length) cmd += ` --lora-paths ${_mlxLoraPaths.map(_shellQuote).join(' ')}`;
|
||||
const _mlxLoraScales = _listField(f.mlx_lora_scales).filter(s => /^-?\d+(?:\.\d+)?$/.test(s));
|
||||
if (_mlxLoraScales.length) cmd += ` --lora-scales ${_mlxLoraScales.map(_shellQuote).join(' ')}`;
|
||||
} else if (backend === 'mlx') {
|
||||
const mlxPy = _isWindows() ? 'python' : _py3Bin;
|
||||
const mlxHost = f.host ? '0.0.0.0' : '127.0.0.1';
|
||||
cmd += `${mlxPy} -m mlx_lm.server --model ${_shellQuote(modelName)} --host ${mlxHost} --port ${f.port || '8080'}`;
|
||||
const mlxMaxTokens = String(f.ctx || '').trim();
|
||||
if (/minimax|mini-max/i.test(modelName)) {
|
||||
cmd += ' --temp 0.7 --top-p 0.9 --max-tokens 2048';
|
||||
cmd += ` --temp 0.7 --top-p 0.9 --max-tokens ${mlxMaxTokens || '2048'}`;
|
||||
} else if (/^\d+$/.test(mlxMaxTokens)) {
|
||||
// MLX-LM server has no vLLM-style --context-length flag. The closest
|
||||
// server-side request budget it exposes is --max-tokens, so wire the
|
||||
// Cookbook Context/Auto control there for MLX launches.
|
||||
cmd += ` --max-tokens ${mlxMaxTokens}`;
|
||||
}
|
||||
}
|
||||
return cmd;
|
||||
@@ -1041,19 +1092,20 @@ async function _fetchDependencies() {
|
||||
try {
|
||||
// Resolve the target server from the deps dropdown so remote-target
|
||||
// packages are checked on THAT server's venv (not just the local host).
|
||||
let _depHost = '', _depPort = '', _depVenv = '';
|
||||
let _depHost = '', _depPort = '', _depVenv = '', _depPlatform = '';
|
||||
const _dsel = document.getElementById('hwfit-deps-server');
|
||||
const _depSrv = _dsel && _dsel.value !== 'local' ? _serverByVal(_dsel.value) : null;
|
||||
if (_depSrv) {
|
||||
_depHost = _depSrv.host || ''; _depPort = _depSrv.port || ''; _depVenv = _depSrv.envPath || '';
|
||||
_depHost = _depSrv.host || ''; _depPort = _depSrv.port || ''; _depVenv = _depSrv.envPath || ''; _depPlatform = _depSrv.platform || '';
|
||||
} else if (_envState.remoteHost) {
|
||||
_depHost = _envState.remoteHost; _depPort = _getPort(_envState.remoteHost) || ''; _depVenv = _envState.envPath || '';
|
||||
_depHost = _envState.remoteHost; _depPort = _getPort(_envState.remoteHost) || ''; _depVenv = _envState.envPath || ''; _depPlatform = _envState.platform || '';
|
||||
}
|
||||
const _pkgParams = new URLSearchParams();
|
||||
if (_depHost) {
|
||||
_pkgParams.set('host', _depHost);
|
||||
if (_depPort) _pkgParams.set('ssh_port', _depPort);
|
||||
if (_depVenv) _pkgParams.set('venv', _depVenv);
|
||||
if (_depPlatform) _pkgParams.set('platform', _depPlatform);
|
||||
}
|
||||
// Pass the detected backend so the server can build a single
|
||||
// OS+backend-aware install command per row (e.g. add nvidia-cuda-toolkit
|
||||
@@ -1063,6 +1115,13 @@ async function _fetchDependencies() {
|
||||
if (_depBackend && _hwfitCache?._scannedHost === _depHost) {
|
||||
_pkgParams.set('backend', _depBackend);
|
||||
}
|
||||
if (_cachedModelIds && _cachedModelIds.size) {
|
||||
const _hint = Array.from(_cachedModelIds)
|
||||
.filter(id => /krea/i.test(String(id || '')))
|
||||
.slice(0, 20)
|
||||
.join(',');
|
||||
if (_hint) _pkgParams.set('model_hint', _hint);
|
||||
}
|
||||
const resp = await fetch('/api/cookbook/packages' + (_pkgParams.toString() ? '?' + _pkgParams.toString() : ''));
|
||||
const data = await resp.json();
|
||||
const pkgs = data.packages || [];
|
||||
@@ -1098,9 +1157,13 @@ async function _fetchDependencies() {
|
||||
vllm: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 4l7 16 7-16"/><path d="M14 4l4 9 3-9"/></svg>',
|
||||
sglang: '<span aria-hidden="true" style="display:block;width:13px;height:13px;background:currentColor;-webkit-mask:url(/static/icons/sglang-mark.png) center/contain no-repeat;mask:url(/static/icons/sglang-mark.png) center/contain no-repeat;"></span>',
|
||||
mlx_lm: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 18V6l4 7 4-7v12"/><path d="M16 6v12"/><path d="M20 6v12"/></svg>',
|
||||
mflux: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>',
|
||||
boogu_image_mlx: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 17c2.5-4 4.5-4 7 0"/><circle cx="9" cy="9" r="1"/><circle cx="15" cy="9" r="1"/></svg>',
|
||||
llama_cpp: '<svg width="13" height="13" viewBox="0 0 600 600" fill="none" aria-hidden="true"><path d="M600 392L504.249 558L504.137 557.929C487.252 584.069 458.193 600 426.864 600H120L240 392H600Z" fill="currentColor"/><path d="M240 392H0L199.602 46.0254C216.032 17.5463 246.411 0 279.29 0H466.154L240 392Z" fill="currentColor"/></svg>',
|
||||
ollama: '<img src="/static/icons/ollama-mark-crop.png" alt="" aria-hidden="true" width="13" height="13" style="display:block;width:13px;height:13px;object-fit:contain;" />',
|
||||
diffusers: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M5 19l2-2M17 7l2-2"/></svg>',
|
||||
krea_diffusers: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 19V5"/><path d="M4 12h4"/><path d="M12 5l-7 7 7 7"/><path d="M14 19l3-14 3 14"/><path d="M15.3 13h3.4"/></svg>',
|
||||
sam_mask: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7c3-3 13-3 16 0"/><path d="M4 17c3 3 13 3 16 0"/><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3"/></svg>',
|
||||
};
|
||||
const _depGlyphHtml = (name) => {
|
||||
const g = _DEP_GLYPHS[name];
|
||||
@@ -1143,23 +1206,6 @@ async function _fetchDependencies() {
|
||||
const _buildDepsBtn = _bdm.length
|
||||
? `<button type="button" class="cookbook-dep-tag cookbook-dep-install cookbook-dep-install-sysdeps" data-dep-sysdeps="${esc(_bdm.join(','))}" data-dep-target="${isLocal ? 'local' : 'remote'}" title="Install ${esc(_bdm.join(', '))} via the OS package manager on this target (requires passwordless sudo or root).">Install build deps</button>`
|
||||
: '';
|
||||
// Render the target-specific install command as a compact mono box
|
||||
// when the server resolved it (target's /etc/os-release was readable
|
||||
// AND the backend is known). The box doubles as the source of truth
|
||||
// for the "Install build deps" button's failure toast — both surfaces
|
||||
// show the same string for the same target.
|
||||
const _instCmd = (_bdm.length && pkg.install_cmd_for_target) ? String(pkg.install_cmd_for_target) : '';
|
||||
const _instCmdOs = pkg.install_cmd_os ? String(pkg.install_cmd_os) : '';
|
||||
const _instCmdBe = pkg.install_cmd_backend ? String(pkg.install_cmd_backend) : '';
|
||||
const _instLabel = (_instCmdOs && _instCmdBe) ? `${_instCmdOs} + ${_instCmdBe}` : (_instCmdOs || _instCmdBe || 'this target');
|
||||
const _instCmdBox = _instCmd
|
||||
? `<div class="cookbook-dep-install-cmd" data-dep-cmd="${esc(_instCmd)}" style="margin-top:6px;font-size:10.5px;opacity:0.85;">`
|
||||
+ `<div style="opacity:0.65;margin-bottom:2px;">Install on ${esc(_instLabel)}:</div>`
|
||||
+ `<div style="display:flex;gap:4px;align-items:stretch;">`
|
||||
+ `<code style="flex:1;padding:4px 6px;background:color-mix(in srgb, var(--fg) 6%, transparent);border:1px solid var(--border);border-radius:4px;font-family:var(--mono, ui-monospace, monospace);font-size:10.5px;white-space:pre-wrap;word-break:break-all;">${esc(_instCmd)}</code>`
|
||||
+ `<button type="button" class="cookbook-dep-cmd-copy" data-dep-cmd-copy="${esc(_instCmd)}" title="Copy install command" style="padding:2px 8px;font-size:10px;border:1px solid var(--border);border-radius:4px;background:none;cursor:pointer;color:var(--fg-muted);">Copy</button>`
|
||||
+ `</div></div>`
|
||||
: '';
|
||||
// Partial-state row (replaces the cryptic yellow "Partial ▾" tag).
|
||||
// Renders inline as a yellow banner with two clear actions: one-tap
|
||||
// Install (runs the reinstall in cookbook) or Copy command (paste
|
||||
@@ -1179,7 +1225,6 @@ async function _fetchDependencies() {
|
||||
+ `<div class="memory-item-meta" style="font-size:10px;opacity:0.5;margin-top:2px;">${esc(pkg.desc)}</div>`
|
||||
+ note
|
||||
+ updateNote
|
||||
+ _instCmdBox
|
||||
+ `</div>`
|
||||
+ _rebuildBtn
|
||||
+ _buildDepsBtn
|
||||
@@ -1194,13 +1239,21 @@ async function _fetchDependencies() {
|
||||
// the user sees a paste-ready sequence; Run keeps using env_prefix to
|
||||
// activate the same venv before the pip command. Docker variant skips
|
||||
// the activate line — `docker pull` doesn't need a venv.
|
||||
function _recipeRuntimeCommands(commands, variant) {
|
||||
if (variant === 'docker') return commands;
|
||||
const envPath = (_envState.envPath || '').replace(/\/+$/, '');
|
||||
if (_envState.env !== 'venv' || !envPath) return commands;
|
||||
const py = _shellQuote(`${envPath}/bin/python3`);
|
||||
return commands.map(cmd => String(cmd || '').replace(/^python(\s+-m\s+pip\b)/, `${py}$1`));
|
||||
}
|
||||
function _recipeDisplayText(commands, variant) {
|
||||
const runtimeCommands = _recipeRuntimeCommands(commands, variant);
|
||||
if (variant === 'docker') return commands.join('\n');
|
||||
const envPath = (_envState.envPath || '').replace(/\/+$/, '');
|
||||
const activate = envPath
|
||||
? `source ${envPath}${envPath.endsWith('/bin/activate') ? '' : '/bin/activate'}`
|
||||
: '# (activate your venv first)';
|
||||
return [activate, ...commands].join('\n');
|
||||
return [activate, ...runtimeCommands].join('\n');
|
||||
}
|
||||
|
||||
// Per-backend recipe panel (model picker + commands + Copy/Run).
|
||||
@@ -1224,18 +1277,19 @@ async function _fetchDependencies() {
|
||||
const initial = pickRecipe(backend, '') || candidates[0];
|
||||
const initialVariant = RECIPE_DEFAULT_VARIANT;
|
||||
const initialCmds = recipeCommands(initial, initialVariant);
|
||||
const initialRuntimeCmds = _recipeRuntimeCommands(initialCmds, initialVariant);
|
||||
const rightActive = initialVariant === 'docker' ? ' mode-right' : '';
|
||||
return `<div class="cookbook-dep-recipe-panel" data-dep-recipe-panel="${esc(backend)}" data-dep-recipe-active-variant="${esc(initialVariant)}" style="display:none;margin:-4px 0 8px;padding:8px 12px 10px;background:rgba(0,0,0,0.04);border:1px solid var(--border);border-top:none;border-radius:0 0 6px 6px;">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
|
||||
<span style="font-size:11px;opacity:0.75;flex-shrink:0;">Serving which model?</span>
|
||||
<select class="settings-select cookbook-dep-recipe-pick" data-dep-recipe-pick="${esc(backend)}" style="flex:1;font-size:11px;padding:3px 6px;">${opts}</select>
|
||||
<div class="mode-toggle${rightActive}" data-dep-recipe-variants="${esc(backend)}" style="flex-shrink:0;">
|
||||
<button type="button" class="mode-toggle-btn${initialVariant === 'pip' ? ' active' : ''}" data-dep-recipe-variant="${esc(backend)}" data-variant="pip" aria-pressed="${initialVariant === 'pip'}">Pip/uv</button>
|
||||
<button type="button" class="mode-toggle-btn${initialVariant === 'pip' ? ' active' : ''}" data-dep-recipe-variant="${esc(backend)}" data-variant="pip" aria-pressed="${initialVariant === 'pip'}">Pip</button>
|
||||
<button type="button" class="mode-toggle-btn${initialVariant === 'docker' ? ' active' : ''}" data-dep-recipe-variant="${esc(backend)}" data-variant="docker" aria-pressed="${initialVariant === 'docker'}">Docker</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position:relative;">
|
||||
<pre class="cookbook-dep-recipe-cmds" data-dep-recipe-cmds="${esc(backend)}" data-dep-recipe-install="${esc(initialCmds.join('\n'))}" style="margin:0;padding:8px 36px 8px 10px;background:rgba(0,0,0,0.08);border-radius:4px;font-size:11px;line-height:1.5;overflow-x:auto;white-space:pre;">${esc(_recipeDisplayText(initialCmds, initialVariant))}</pre>
|
||||
<pre class="cookbook-dep-recipe-cmds" data-dep-recipe-cmds="${esc(backend)}" data-dep-recipe-install="${esc(initialRuntimeCmds.join('\n'))}" style="margin:0;padding:8px 36px 8px 10px;background:rgba(0,0,0,0.08);border-radius:4px;font-size:11px;line-height:1.5;overflow-x:auto;white-space:pre;">${esc(_recipeDisplayText(initialCmds, initialVariant))}</pre>
|
||||
<button type="button" id="recipe-copy-${esc(backend)}" class="cookbook-dep-recipe-copy" data-dep-recipe-copy="${esc(backend)}" title="Copy" aria-label="Copy" style="position:absolute;top:6px;right:6px;padding:3px 5px;background:none;border:none;color:inherit;opacity:0.7;cursor:pointer;display:inline-flex;align-items:center;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;justify-content:flex-end;margin-top:6px;">
|
||||
@@ -1244,18 +1298,99 @@ async function _fetchDependencies() {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const _rowsHtml = (items) => items.map(_depRow).join('');
|
||||
const _sectionHeader = (title, note) =>
|
||||
`<div class="cookbook-dep-section"><span class="cookbook-dep-section-title">${title}</span><span class="cookbook-dep-section-note">${note}</span></div>`;
|
||||
const _section = (title, note, items) =>
|
||||
items.length
|
||||
? `<div class="cookbook-dep-section"><span class="cookbook-dep-section-title">${title}</span><span class="cookbook-dep-section-note">${note}</span></div>` + items.map(_depRow).join('')
|
||||
: '';
|
||||
items.length ? _sectionHeader(title, note) + _rowsHtml(items) : '';
|
||||
const _pkgOrder = {
|
||||
System: ['tmux', 'docker'],
|
||||
Tools: ['hf_transfer'],
|
||||
LLM: ['llama_cpp', 'sglang', 'vllm', 'mlx_lm'],
|
||||
Image: ['diffusers', 'krea_diffusers', 'transformers', 'sam_mask', 'mflux', 'boogu_image_mlx', 'mlx_vlm'],
|
||||
};
|
||||
const _sortDeps = (items, category) => {
|
||||
const order = _pkgOrder[category] || [];
|
||||
return [...items].sort((a, b) => {
|
||||
const ai = order.indexOf(a.name);
|
||||
const bi = order.indexOf(b.name);
|
||||
const ar = ai === -1 ? 999 : ai;
|
||||
const br = bi === -1 ? 999 : bi;
|
||||
return ar - br || String(a.name || '').localeCompare(String(b.name || ''));
|
||||
});
|
||||
};
|
||||
const _serverDepsHtml = (items) => {
|
||||
const byCat = new Map();
|
||||
for (const item of items) {
|
||||
const cat = item.category || 'Other';
|
||||
if (!byCat.has(cat)) byCat.set(cat, []);
|
||||
byCat.get(cat).push(item);
|
||||
}
|
||||
const parts = [];
|
||||
const order = ['System', 'Tools', 'Image', 'LLM', 'Audio', 'Other'];
|
||||
for (const cat of order) {
|
||||
const catItems = _sortDeps(byCat.get(cat) || [], cat);
|
||||
if (!catItems.length) continue;
|
||||
if (cat === 'Image') {
|
||||
const mlxNames = new Set(['mflux', 'boogu_image_mlx', 'mlx_vlm']);
|
||||
const general = catItems.filter(p => !mlxNames.has(p.name));
|
||||
const mlx = catItems.filter(p => mlxNames.has(p.name));
|
||||
parts.push(_sectionHeader('Image', 'Diffusers and shared image tooling.'));
|
||||
if (general.length) parts.push(_rowsHtml(general));
|
||||
if (mlx.length) {
|
||||
parts.push(
|
||||
`<div class="cookbook-dep-subgroup">`
|
||||
+ `<div class="cookbook-dep-subgroup-title"><span>MLX image runtimes</span><em>Apple Silicon only</em></div>`
|
||||
+ _rowsHtml(mlx)
|
||||
+ `</div>`
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const note = cat === 'System'
|
||||
? 'OS tools needed for background tasks.'
|
||||
: cat === 'LLM'
|
||||
? 'Text model serving engines and download helpers.'
|
||||
: cat === 'Tools'
|
||||
? 'Browser and assistant utilities.'
|
||||
: '';
|
||||
parts.push(_section(cat, note, catItems));
|
||||
}
|
||||
return parts.join('');
|
||||
};
|
||||
const _appDepsHtml = (items) => {
|
||||
if (!items.length) return '';
|
||||
const byCat = new Map();
|
||||
for (const item of items) {
|
||||
const cat = item.category || 'Other';
|
||||
if (!byCat.has(cat)) byCat.set(cat, []);
|
||||
byCat.get(cat).push(item);
|
||||
}
|
||||
const parts = [_sectionHeader('Odysseus app', 'Run inside the Odysseus app itself.')];
|
||||
const order = ['System', 'Tools', 'Image', 'LLM', 'Audio', 'Other'];
|
||||
for (const cat of order) {
|
||||
const catItems = _sortDeps(byCat.get(cat) || [], cat);
|
||||
if (!catItems.length) continue;
|
||||
const note = cat === 'LLM'
|
||||
? 'Local app model helpers.'
|
||||
: cat === 'Image'
|
||||
? 'Editor image tools.'
|
||||
: cat === 'Tools'
|
||||
? 'Browser and assistant utilities.'
|
||||
: '';
|
||||
parts.push(_section(cat, note, catItems));
|
||||
}
|
||||
return parts.join('');
|
||||
};
|
||||
|
||||
const _viewingRemote = !!(_dsel && _dsel.value && _dsel.value !== 'local');
|
||||
const _appDeps = pkgs.filter(p => p.target === 'local');
|
||||
const _serverDeps = pkgs.filter(p => p.target !== 'local');
|
||||
const _visibleDep = (p) => p.applicable !== false || p.installed || (p.kind === 'system' && p.name !== 'APFEL');
|
||||
const _appDeps = pkgs.filter(p => p.target === 'local' && _visibleDep(p));
|
||||
const _serverDeps = pkgs.filter(p => p.target !== 'local' && _visibleDep(p));
|
||||
|
||||
list.innerHTML = [
|
||||
_viewingRemote ? '' : _section('Odysseus app', 'Run inside the Odysseus app itself.', _appDeps),
|
||||
_section('Server', 'Run on the server chosen above (Local, or a remote box over SSH).', _serverDeps),
|
||||
_viewingRemote ? '' : _appDepsHtml(_appDeps),
|
||||
_serverDepsHtml(_serverDeps),
|
||||
].join('');
|
||||
|
||||
// Shared install/update routine — used by the Install button and the
|
||||
@@ -1275,8 +1410,11 @@ async function _fetchDependencies() {
|
||||
}
|
||||
}
|
||||
const targetHost = isLocalOnly ? 'this server' : ((targetServer?.host || _envState.remoteHost) || 'local');
|
||||
const targetEnv = isLocalOnly ? 'none' : (targetServer?.env || _envState.env || 'none');
|
||||
let targetEnv = isLocalOnly ? 'none' : (targetServer?.env || _envState.env || 'none');
|
||||
const targetEnvPath = isLocalOnly ? '' : (targetServer?.envPath || _envState.envPath || '');
|
||||
if (!isLocalOnly && targetEnvPath && (!targetEnv || targetEnv === 'none')) {
|
||||
targetEnv = /(?:^|\/)(?:\.?venv|env)(?:\/|$)|\/bin\/activate$/i.test(targetEnvPath) ? 'venv' : targetEnv;
|
||||
}
|
||||
const targetPlatform = isLocalOnly ? (_envState.hostPlatform || _envState.platform || '') : (targetServer?.platform || _envState.platform || '');
|
||||
const targetRemoteHost = isLocalOnly ? '' : (targetServer?.host || _envState.remoteHost || '');
|
||||
// Always go through `python -m pip` so the leading token is `python`
|
||||
@@ -1300,7 +1438,14 @@ async function _fetchDependencies() {
|
||||
} else {
|
||||
_py = 'python3';
|
||||
}
|
||||
const cmd = `${_py} -m pip install${upgrade ? ' -U' : ''}${_pipFlags} "${pipName}"`;
|
||||
const pipArgs = String(pipName || '')
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map(_shellQuote)
|
||||
.join(' ');
|
||||
const depTaskId = String(pkgName || pipName || 'dependency').trim().replace(/\s+/g, '_');
|
||||
const cmd = `${_py} -m pip install${upgrade ? ' -U' : ''}${_pipFlags} ${pipArgs}`;
|
||||
let envPrefix = '';
|
||||
if (_isWindows()) {
|
||||
if (targetEnv === 'venv' && targetEnvPath) {
|
||||
@@ -1318,7 +1463,7 @@ async function _fetchDependencies() {
|
||||
}
|
||||
try {
|
||||
const reqBody = {
|
||||
repo_id: pipName,
|
||||
repo_id: depTaskId,
|
||||
cmd: cmd,
|
||||
remote_host: targetRemoteHost || undefined,
|
||||
ssh_port: _getPort(targetRemoteHost) || undefined,
|
||||
@@ -1347,7 +1492,7 @@ async function _fetchDependencies() {
|
||||
}
|
||||
// _dep flags this as a pip dependency/driver install (not a servable
|
||||
// model) so the running-task card doesn't offer a "Serve →" button.
|
||||
const payload = { repo_id: pipName, _cmd: cmd, remote_host: targetRemoteHost || '', _dep: true, env_path: targetEnvPath || '', platform: targetPlatform || '' };
|
||||
const payload = { repo_id: depTaskId, _cmd: cmd, remote_host: targetRemoteHost || '', _dep: true, env_path: targetEnvPath || '', platform: targetPlatform || '' };
|
||||
_addTask(data.session_id, 'pip ' + pkgName, 'download', payload);
|
||||
if (statusEl) { statusEl.textContent = upgrade ? 'Updating...' : 'Installing...'; statusEl.disabled = true; }
|
||||
uiModule.showToast(`${upgrade ? 'Updating' : 'Installing'} ${pkgName} on ${targetHost}...`);
|
||||
@@ -1422,9 +1567,8 @@ async function _fetchDependencies() {
|
||||
});
|
||||
});
|
||||
|
||||
// Inline command-box "Copy" buttons — one per row that has a
|
||||
// resolved per-target install command. Same string surfaces here
|
||||
// and in the toast/diagnosis so the user always sees one answer.
|
||||
// Inline command "Copy" buttons, currently used by targeted recipe
|
||||
// repair panels such as the llama.cpp CUDA wheel reinstall.
|
||||
list.querySelectorAll('.cookbook-dep-cmd-copy').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -1443,12 +1587,6 @@ async function _fetchDependencies() {
|
||||
const names = (btn.dataset.depSysdeps || '').split(',').map(s => s.trim()).filter(Boolean);
|
||||
if (!names.length) return;
|
||||
const isLocal = btn.dataset.depTarget === 'local';
|
||||
// Pull the per-target install command from the sibling box on
|
||||
// the same row, so failure toasts surface the SAME line the
|
||||
// user already sees inline. No duplicated formatting logic.
|
||||
const _row = btn.closest('.cookbook-dep-row');
|
||||
const _cmdBox = _row?.querySelector('.cookbook-dep-install-cmd');
|
||||
const _resolvedCmd = _cmdBox?.dataset.depCmd || '';
|
||||
// Mirror _installDep: the Dependencies tab has its own server
|
||||
// picker that can override _envState. Apply it before reading
|
||||
// remoteHost, otherwise the install silently runs on the wrong
|
||||
@@ -1481,18 +1619,10 @@ async function _fetchDependencies() {
|
||||
try { await _fetchDependencies(); } catch {}
|
||||
} else {
|
||||
const reason = data.error || data.detail || `HTTP ${res.status}`;
|
||||
// Append the per-target install command (if we already know it
|
||||
// from the row) so the user can copy-paste it without leaving
|
||||
// the toast. Otherwise just surface the error.
|
||||
const _suffix = _resolvedCmd ? `\n\nRun on ${targetLabel}: ${_resolvedCmd}` : '';
|
||||
uiModule.showToast('System dependency install failed: ' + String(reason).slice(0, 300) + _suffix, {
|
||||
uiModule.showToast('System dependency install failed: ' + String(reason).slice(0, 300), {
|
||||
duration: 25000,
|
||||
action: _resolvedCmd ? 'Copy command' : 'OK',
|
||||
onAction: async () => {
|
||||
if (_resolvedCmd) {
|
||||
try { await navigator.clipboard.writeText(_resolvedCmd); } catch {}
|
||||
}
|
||||
},
|
||||
action: 'OK',
|
||||
onAction: () => {},
|
||||
});
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
@@ -1532,17 +1662,18 @@ async function _fetchDependencies() {
|
||||
const sel = panel.querySelector('[data-dep-recipe-pick]');
|
||||
const recipe = pickRecipe(backend, (sel && sel.value) || '');
|
||||
const cmds = recipeCommands(recipe, variant);
|
||||
const runtimeCmds = _recipeRuntimeCommands(cmds, variant);
|
||||
const pre = panel.querySelector('[data-dep-recipe-cmds]');
|
||||
if (pre) {
|
||||
pre.textContent = _recipeDisplayText(cmds, variant);
|
||||
pre.dataset.depRecipeInstall = cmds.join('\n');
|
||||
pre.dataset.depRecipeInstall = runtimeCmds.join('\n');
|
||||
}
|
||||
}
|
||||
// Model select: pickRecipe matches the model id against the catalog.
|
||||
list.querySelectorAll('[data-dep-recipe-pick]').forEach(sel => {
|
||||
sel.addEventListener('change', () => _refreshRecipePre(sel.dataset.depRecipePick));
|
||||
});
|
||||
// Variant toggle (Pip/uv vs Docker): mirrors the agent/chat mode-toggle
|
||||
// Variant toggle (Pip vs Docker): mirrors the agent/chat mode-toggle
|
||||
// pattern — buttons get .active, container gets .mode-right when the
|
||||
// right slot is selected so the sliding pill animates over.
|
||||
list.querySelectorAll('[data-dep-recipe-variant]').forEach(btn => {
|
||||
@@ -1595,16 +1726,26 @@ async function _fetchDependencies() {
|
||||
// displayed source line is for the user's reading; env_prefix
|
||||
// handles it for the actual run.
|
||||
const installRaw = pre.dataset.depRecipeInstall || pre.textContent;
|
||||
const cmd = installRaw.split('\n').map(s => s.trim()).filter(Boolean).join(' && ');
|
||||
const depsSel = document.getElementById('hwfit-deps-server');
|
||||
if (depsSel) _applyServerSelection(depsSel.value);
|
||||
const targetHost = _envState.remoteHost || 'local';
|
||||
const inferredVenv = _envState.envPath && (!_envState.env || _envState.env === 'none')
|
||||
&& /(?:^|\/)(?:\.?venv|env)(?:\/|$)|\/bin\/activate$/i.test(_envState.envPath);
|
||||
const recipeEnv = inferredVenv ? 'venv' : _envState.env;
|
||||
const recipePy = (recipeEnv === 'venv' && _envState.envPath)
|
||||
? `${_envState.envPath.replace(/\/+$/, '').replace(/\/bin\/activate$/i, '')}/bin/python3`
|
||||
: '';
|
||||
const cmd = installRaw.split('\n').map(s => {
|
||||
let line = s.trim();
|
||||
if (recipePy) line = line.replace(/^python(?:3)?\s+-m\s+pip\b/, `${recipePy} -m pip`);
|
||||
return line;
|
||||
}).filter(Boolean).join(' && ');
|
||||
// Build env_prefix from the configured envPath (matches _installDep).
|
||||
let envPrefix = '';
|
||||
if (_envState.env === 'venv' && _envState.envPath) {
|
||||
if (recipeEnv === 'venv' && _envState.envPath) {
|
||||
const p = _envState.envPath;
|
||||
envPrefix = 'source ' + _shellQuote(p.endsWith('/bin/activate') ? p : p + '/bin/activate');
|
||||
} else if (_envState.env === 'conda' && _envState.envPath) {
|
||||
} else if (recipeEnv === 'conda' && _envState.envPath) {
|
||||
envPrefix = 'eval "$(conda shell.bash hook)" && conda activate ' + _shellQuote(_envState.envPath);
|
||||
}
|
||||
const reqBody = {
|
||||
@@ -2013,6 +2154,27 @@ function _wireTabEvents(body) {
|
||||
hwRefreshBtn.addEventListener('click', _refreshScanDownloadTarget);
|
||||
}
|
||||
|
||||
const hwAdvancedBtn = document.getElementById('hwfit-advanced-btn');
|
||||
const hwAdvancedPanel = document.getElementById('hwfit-advanced-panel');
|
||||
if (hwAdvancedBtn && hwAdvancedPanel && !hwAdvancedBtn.dataset.bound) {
|
||||
hwAdvancedBtn.dataset.bound = '1';
|
||||
const setAdvancedOpen = (open) => {
|
||||
hwAdvancedPanel.classList.toggle('hidden', !open);
|
||||
hwAdvancedBtn.classList.toggle('active', open);
|
||||
hwAdvancedBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
};
|
||||
hwAdvancedBtn.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
setAdvancedOpen(hwAdvancedPanel.classList.contains('hidden'));
|
||||
});
|
||||
hwAdvancedPanel.addEventListener('click', (ev) => ev.stopPropagation());
|
||||
document.addEventListener('click', () => setAdvancedOpen(false));
|
||||
document.addEventListener('keydown', (ev) => {
|
||||
if (ev.key === 'Escape') setAdvancedOpen(false);
|
||||
});
|
||||
}
|
||||
|
||||
const editDirsLink = document.querySelector('.cookbook-serve-dir-edit');
|
||||
if (editDirsLink) {
|
||||
editDirsLink.addEventListener('click', () => {
|
||||
@@ -2926,15 +3088,39 @@ function _renderRecipes() {
|
||||
html += '</div>';
|
||||
html += '<p class="memory-desc doclib-desc" style="margin-top:6px;">Scans your hardware for what models you can run. Hardware is cached; hit the scan button to re-probe after changing GPUs.</p>';
|
||||
html += '<div class="hwfit-toolbar" style="margin-top:9px;">';
|
||||
html += '<select class="cookbook-field-input hwfit-usecase" id="hwfit-usecase" style="height:28px;">';
|
||||
html += '<option value="general" selected>Standard</option>';
|
||||
// Image tab removed — text→image gen is gone from this build (only inpaint
|
||||
// remains, which uses its own settings panel). Vision (multimodal) stays.
|
||||
html += '<option value="multimodal">Vision</option></select>';
|
||||
// Search moved next to the Type filter so the two primary picks
|
||||
// (what category + free text) sit together; the more advanced
|
||||
// levers (Engine / Quant / Context) live to the right.
|
||||
html += '<select class="cookbook-field-input hwfit-server-select" id="hwfit-server-select" style="height:28px;min-width:88px;position:relative;top:0px;">';
|
||||
html += _buildServerOpts(false);
|
||||
html += '</select>';
|
||||
// Keep the main scan toolbar light: server + free-text search. Advanced
|
||||
// levers (Engine / Quant / Context) live behind the cog beside Refresh.
|
||||
html += '<input type="text" class="cookbook-field-input hwfit-search" id="hwfit-search" placeholder="Search models..." style="flex:1;" />';
|
||||
html += '</div>';
|
||||
html += '<div class="hwfit-toolbar" style="margin-top:7px;">';
|
||||
html += '<span class="hwfit-usecase-wrap">';
|
||||
html += '<select class="cookbook-field-input hwfit-usecase" id="hwfit-usecase" style="display:none;height:28px;">';
|
||||
html += '<option value="general" selected>Standard</option>';
|
||||
html += '<option value="multimodal">Vision</option>';
|
||||
html += '<option value="image_gen">Image</option></select>';
|
||||
html += '<button type="button" class="cookbook-field-input hwfit-usecase-btn" data-hwfit-usecase-btn aria-haspopup="listbox" aria-expanded="false" title="Model type">';
|
||||
html += '<span class="hwfit-usecase-btn-icon" data-hwfit-usecase-icon aria-hidden="true"></span>';
|
||||
html += '<span class="hwfit-usecase-btn-label" data-hwfit-usecase-label>Standard</span>';
|
||||
html += '<svg class="hwfit-usecase-caret" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
||||
html += '</button>';
|
||||
html += '<div class="hwfit-usecase-menu" data-hwfit-usecase-menu role="listbox" hidden></div>';
|
||||
html += '</span>';
|
||||
html += '<div class="hwfit-gpu-toggles" id="hwfit-gpu-toggles"></div>';
|
||||
html += '<button type="button" class="hwfit-gpu-btn hwfit-hw-manual-btn" id="hwfit-hw-manual-btn" title="Set hardware manually" style="flex-shrink:0;position:relative;top:-3px;left:-1px;display:inline-flex;align-items:center;gap:3px;"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>EDIT</button>';
|
||||
html += '<button type="button" class="hwfit-gpu-btn hwfit-advanced-btn" id="hwfit-advanced-btn" title="Scan settings" aria-label="Scan settings" aria-expanded="false" style="flex-shrink:0;position:relative;top:-3px;left:-3px;width:26px;height:26px;padding:0;display:inline-flex;align-items:center;justify-content:center;"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15.5A3.5 3.5 0 1 0 12 8a3.5 3.5 0 0 0 0 7.5Z"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06A2 2 0 1 1 7.04 4.3l.06.06A1.65 1.65 0 0 0 8.92 4a1.65 1.65 0 0 0 1-1.51V2a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z"/></svg></button>';
|
||||
html += '<button type="button" class="hwfit-gpu-btn hwfit-hw-refresh-btn" id="hwfit-hw-refresh-btn" title="Refresh selected server hardware and cached models" aria-label="Refresh selected server hardware and cached models" style="flex-shrink:0;position:relative;top:-3px;left:-5px;width:26px;height:26px;padding:0;display:inline-flex;align-items:center;justify-content:center;"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 4v6h6"/><path d="M23 20v-6h-6"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10"/><path d="M3.51 15a9 9 0 0 0 14.85 3.36L23 14"/></svg></button>';
|
||||
// Sort state — the clickable column headers read/write this (pewds' original
|
||||
// sort paradigm). Newest is reachable by clicking the Model column header.
|
||||
html += '<select class="cookbook-field-input hwfit-sort" id="hwfit-sort" style="display:none">';
|
||||
html += '<option value="newest" selected>Latest</option>';
|
||||
html += '<option value="fit">Fit</option><option value="score">Score</option><option value="vram">VRAM</option>';
|
||||
html += '<option value="speed">Speed</option><option value="params">Params</option>';
|
||||
html += '<option value="context">Context</option></select>';
|
||||
html += '</div>';
|
||||
html += '<div class="hwfit-advanced-panel hidden" id="hwfit-advanced-panel" aria-label="Scan settings">';
|
||||
html += '<span class="hwfit-engine-wrap">';
|
||||
html += '<select class="cookbook-field-input hwfit-engine" id="hwfit-engine" style="display:none;" title="Filter by serving engine">';
|
||||
html += '<option value="">Engine</option>';
|
||||
@@ -2971,21 +3157,6 @@ function _renderRecipes() {
|
||||
html += '<span>Context</span><span class="hwfit-help-chip hwfit-help-chip-inline" title="Context length. Lower it to find more models that could fit your hardware; raise it when you need longer chats or documents.">?</span><input type="range" id="hwfit-context" min="0" max="5" step="1" value="3" />';
|
||||
html += '<output id="hwfit-context-label">50k</output></label>';
|
||||
html += '</div>';
|
||||
html += '<div class="hwfit-toolbar" style="margin-top:7px;">';
|
||||
html += '<select class="cookbook-field-input hwfit-server-select" id="hwfit-server-select" style="height:28px;min-width:88px;position:relative;top:0px;">';
|
||||
html += _buildServerOpts(false);
|
||||
html += '</select>';
|
||||
html += '<div class="hwfit-gpu-toggles" id="hwfit-gpu-toggles"></div>';
|
||||
html += '<button type="button" class="hwfit-gpu-btn hwfit-hw-manual-btn" id="hwfit-hw-manual-btn" title="Set hardware manually" style="flex-shrink:0;position:relative;top:-3px;left:-1px;display:inline-flex;align-items:center;gap:3px;"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>EDIT</button>';
|
||||
html += '<button type="button" class="hwfit-gpu-btn hwfit-hw-refresh-btn" id="hwfit-hw-refresh-btn" title="Refresh selected server hardware and cached models" aria-label="Refresh selected server hardware and cached models" style="flex-shrink:0;position:relative;top:-3px;left:-3px;width:26px;height:26px;padding:0;display:inline-flex;align-items:center;justify-content:center;"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 4v6h6"/><path d="M23 20v-6h-6"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10"/><path d="M3.51 15a9 9 0 0 0 14.85 3.36L23 14"/></svg></button>';
|
||||
// Sort state — the clickable column headers read/write this (pewds' original
|
||||
// sort paradigm). Newest is reachable by clicking the Model column header.
|
||||
html += '<select class="cookbook-field-input hwfit-sort" id="hwfit-sort" style="display:none">';
|
||||
html += '<option value="newest" selected>Latest</option>';
|
||||
html += '<option value="fit">Fit</option><option value="score">Score</option><option value="vram">VRAM</option>';
|
||||
html += '<option value="speed">Speed</option><option value="params">Params</option>';
|
||||
html += '<option value="context">Context</option></select>';
|
||||
html += '</div>';
|
||||
html += '<div class="hwfit-manual-panel hidden" id="hwfit-manual-panel">';
|
||||
html += '<span class="hwfit-manual-note" style="font-size:10px;opacity:0.6;width:100%;margin-bottom:2px;">Simulator — these values REPLACE detected hardware.</span>';
|
||||
html += '<select class="hwfit-manual-mode"><option value="gpu">GPU</option><option value="ram">RAM</option></select>';
|
||||
|
||||
Reference in New Issue
Block a user