Files
Will Miao 965bfd33cd feat(controls): keep model counts readable in large libraries
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.
2026-10-18 18:34:16 +08:00

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')),
};
}