Counts in the filter panel chips (base models, tags, model types), the All Types
dropdown and the Refresh scope rows were rendered as raw digits, so a library with
10k+ models turned every chip into a five digit pill that dominated the row it sat
in — and the Refresh menu grouped its numbers while the filter panel did not.
One formatter now renders all of them (`formatCount`): exact and locale-grouped
below 10,000, abbreviated to three significant digits above it, and abbreviated
only where the locale's Intl compact form is a real unit and narrower than the
plain number — German keeps 18.475 instead of the rounded 18.500, Russian keeps
18 475 instead of the wider 18,5 тыс.
Their hover text is always present and always exact ({name} — {count} models, and
... recipes on the Recipes page): an affordance that only exists above a threshold
is undiscoverable, and the tooltip is also what reveals a chip label that CSS has
ellipsized.
Chips gain a two-slot layout (the name ellipsizes, the count keeps a tabular
right-aligned slot) so a long tag name can no longer push the count out of the
chip; Refresh rows move from float:right to a flex row, which keeps the counts in
a right-hand column and lets a long root label ellipsize. The All Types group
tooltip moves onto its trigger so it stops leaking onto the menu options.
The two new strings ship as [TODO: Translate] placeholders in the nine locales;
the translation pass is the next commit.
84 lines
3.1 KiB
JavaScript
84 lines
3.1 KiB
JavaScript
// ScanScopeMenu.js - per-root entries of the Refresh dropdown (issue #1108)
|
|
import { translate } from '../../utils/i18nHelpers.js';
|
|
import { formatCount, formatCountExact } from '../../utils/formatters.js';
|
|
|
|
/**
|
|
* Render one row per configured model root.
|
|
*
|
|
* Rows carry the root path in `data-root` and are wired by delegation, because
|
|
* the list is rebuilt every time the root set changes. An offline root stays
|
|
* clickable (the click explains why nothing can be scanned) and is only
|
|
* de-emphasised visually.
|
|
*
|
|
* @param {HTMLElement|null} menu - Container element (#refreshScopeMenu)
|
|
* @param {Array<Object>} details - `root_details` from GET /api/lm/{type}/roots
|
|
*/
|
|
export function renderScanScopeMenu(menu, details = []) {
|
|
if (!menu) {
|
|
return;
|
|
}
|
|
menu.innerHTML = '';
|
|
|
|
details.forEach(detail => {
|
|
if (!detail || !detail.path) {
|
|
return;
|
|
}
|
|
const item = document.createElement('div');
|
|
item.className = `dropdown-item scan-root-item${detail.reachable ? '' : ' is-offline'}`;
|
|
item.dataset.action = 'scan-root';
|
|
item.dataset.root = detail.path;
|
|
item.title = detail.path;
|
|
|
|
const icon = document.createElement('i');
|
|
icon.className = `fas ${detail.reachable ? 'fa-folder-open' : 'fa-plug-circle-xmark'}`;
|
|
item.appendChild(icon);
|
|
|
|
const label = document.createElement('span');
|
|
label.className = 'scan-root-label';
|
|
label.textContent = detail.label || detail.path;
|
|
item.appendChild(label);
|
|
|
|
const meta = document.createElement('span');
|
|
if (detail.reachable) {
|
|
const count = Number(detail.models || 0);
|
|
// The visible count is abbreviated at large magnitudes, like the
|
|
// filter chips; the row tooltip always names the path and the exact
|
|
// count, so its shape does not change with the library size.
|
|
const exact = formatCountExact(count);
|
|
const shown = formatCount(count);
|
|
meta.className = 'scan-root-count';
|
|
meta.textContent = translate(
|
|
'loras.controls.refresh.rootModels',
|
|
{ count: shown },
|
|
`${shown} models`
|
|
);
|
|
item.title = `${detail.path}\n${translate(
|
|
'loras.controls.refresh.rootModels',
|
|
{ count: exact },
|
|
`${exact} models`
|
|
)}`;
|
|
} else {
|
|
meta.className = 'scan-root-offline';
|
|
meta.textContent = translate('loras.controls.refresh.rootOffline', {}, 'Offline');
|
|
}
|
|
item.appendChild(meta);
|
|
|
|
menu.appendChild(item);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Read what a clicked row refers to.
|
|
* @param {HTMLElement} item - Row rendered by renderScanScopeMenu
|
|
* @returns {{rootPath: string, label: string, offline: boolean}}
|
|
*/
|
|
export function resolveScanScopeTarget(item) {
|
|
const rootPath = item?.dataset?.root || '';
|
|
const label = item?.querySelector?.('.scan-root-label')?.textContent || rootPath;
|
|
return {
|
|
rootPath,
|
|
label,
|
|
offline: Boolean(item?.classList?.contains('is-offline')),
|
|
};
|
|
}
|