Files
ollama-plus/scheduler/public/modelList.js
T

442 lines
13 KiB
JavaScript
Raw Normal View History

2025-09-26 14:28:04 -04:00
function generateModelCard(model, options = {}) {
const mount = options.mountSelector
? /** @type {HTMLElement} */(document.querySelector(options.mountSelector))
: document.body;
if (!mount) throw new Error('mount element not found');
// overlay
const overlay = document.createElement('div');
overlay.className = 'mc-overlay';
overlay.setAttribute('data-testid', 'model-card-overlay');
// dialog shell
const dialog = document.createElement('div');
dialog.className = 'mc-dialog';
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('aria-modal', 'true');
const titleId = `mc-title-${Math.random().toString(36).slice(2)}`,
descId = `mc-desc-${Math.random().toString(36).slice(2)}`;
dialog.setAttribute('aria-labelledby', titleId);
dialog.setAttribute('aria-describedby', descId);
// header
const header = document.createElement('header');
header.className = 'mc-header';
const titleWrap = document.createElement('div');
titleWrap.className = 'mc-titlewrap';
const avatar = document.createElement('img');
avatar.className = 'mc-avatar';
if (model?.info?.meta?.profile_image_url) {
avatar.src = model.info.meta.profile_image_url;
avatar.alt = '';
} else {
avatar.src = 'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
avatar.alt = '';
avatar.classList.add('mc-avatar--placeholder');
}
const h1 = document.createElement('h1');
h1.id = titleId;
h1.className = 'mc-title';
h1.textContent = model?.name ?? model?.id ?? 'model';
const owner = document.createElement('div');
owner.className = 'mc-subtitle';
owner.textContent = model?.owned_by ? `by ${model.owned_by}` : '';
titleWrap.appendChild(avatar);
titleWrap.appendChild(h1);
if (owner.textContent) titleWrap.appendChild(owner);
const closeBtn = document.createElement('button');
closeBtn.className = 'mc-close';
closeBtn.type = 'button';
closeBtn.setAttribute('aria-label', 'close');
closeBtn.textContent = '×';
header.appendChild(titleWrap);
header.appendChild(closeBtn);
// body
const body = document.createElement('div');
body.className = 'mc-body';
// description (primary)
const desc = document.createElement('p');
desc.id = descId;
desc.className = 'mc-desc';
desc.textContent = model?.info?.meta?.description ?? 'no description provided.';
// badges (primary)
const badges = document.createElement('div');
badges.className = 'mc-badges';
const addBadge = (label, value) => {
if (value === undefined || value === null || value === '') return;
const b = document.createElement('span');
b.className = 'mc-badge';
b.textContent = `${label}: ${value}`;
badges.appendChild(b);
};
addBadge('family', model?.ollama?.details?.family ?? model?.ollama?.details?.families?.[0]);
addBadge('params', model?.ollama?.details?.parameter_size);
addBadge('quant', model?.ollama?.details?.quantization_level);
addBadge('format', model?.ollama?.details?.format);
addBadge('connection', model?.connection_type ?? model?.ollama?.connection_type);
addBadge('created', formatEpoch(model?.created));
addBadge('modified', formatIso(model?.ollama?.modified_at));
// capabilities (primary)
const caps = model?.info?.meta?.capabilities ?? {};
const capsWrap = document.createElement('div');
capsWrap.className = 'mc-section';
const capsTitle = document.createElement('h2');
capsTitle.className = 'mc-section-title';
capsTitle.textContent = 'capabilities';
const capsList = document.createElement('ul');
capsList.className = 'mc-capabilities';
Object.entries(caps).forEach(([k, v]) => {
const li = document.createElement('li');
li.className = 'mc-cap';
const label = document.createElement('span');
label.className = 'mc-cap-label';
label.textContent = k.replaceAll('_', ' ');
const state = document.createElement('span');
state.className = `mc-cap-state ${v ? 'is-yes' : 'is-no'}`;
state.textContent = v ? 'yes' : 'no';
li.appendChild(label);
li.appendChild(state);
capsList.appendChild(li);
});
capsWrap.appendChild(capsTitle);
capsWrap.appendChild(capsList);
// advanced details (moved to disclosure)
const adv = document.createElement('details');
adv.className = 'mc-disclosure';
adv.setAttribute('data-testid', 'model-card-advanced');
const advSum = document.createElement('summary');
advSum.className = 'mc-disclosure-summary';
advSum.textContent = 'advanced details';
adv.appendChild(advSum);
const advBody = document.createElement('div');
advBody.className = 'mc-disclosure-body';
// details table (now inside disclosure)
const table = document.createElement('table');
table.className = 'mc-table';
const tbody = document.createElement('tbody'),
rows = [
['id', model?.id],
['object', model?.object],
['digest', model?.ollama?.digest],
['size', formatBytes(model?.ollama?.size)],
['parameter size', model?.ollama?.details?.parameter_size],
['quantization', model?.ollama?.details?.quantization_level],
['base model id', safeText(model?.info?.base_model_id) ?? '—'],
['function calling', safeText(model?.info?.params?.function_calling)],
['active', String(model?.info?.is_active ?? model?.info?.isActive ?? '—')],
];
rows.forEach(([k, v]) => {
if (v === undefined || v === null) return;
const tr = document.createElement('tr');
const th = document.createElement('th');
th.textContent = k;
const td = document.createElement('td');
td.textContent = String(v);
tr.appendChild(th);
tr.appendChild(td);
tbody.appendChild(tr);
});
table.appendChild(tbody);
// urls (inside disclosure; only if valid strings)
const urlSection = document.createElement('div');
urlSection.className = 'mc-section';
if (Array.isArray(model?.ollama?.urls) && model.ollama.urls.some(u => u && typeof u === 'string')) {
const urlsTitle = document.createElement('h2');
urlsTitle.className = 'mc-section-title';
urlsTitle.textContent = 'urls';
const list = document.createElement('ul');
list.className = 'mc-links';
model.ollama.urls.forEach(u => {
if (!u || typeof u !== 'string') return;
const li = document.createElement('li');
const a = document.createElement('a');
a.rel = 'noopener noreferrer';
a.target = '_blank';
a.href = u;
a.textContent = u;
li.appendChild(a);
list.appendChild(li);
});
urlSection.appendChild(urlsTitle);
urlSection.appendChild(list);
}
// raw json (inside disclosure)
const rawWrap = document.createElement('details');
rawWrap.className = 'mc-disclosure mc-disclosure--nested';
const rawSum = document.createElement('summary');
rawSum.className = 'mc-disclosure-summary';
rawSum.textContent = 'raw model json';
rawWrap.appendChild(rawSum);
const pre = document.createElement('pre');
pre.className = 'mc-pre';
pre.textContent = prettyJson(model);
rawWrap.appendChild(pre);
// assemble disclosure body
advBody.appendChild(table);
if (urlSection.childNodes.length) advBody.appendChild(urlSection);
advBody.appendChild(rawWrap);
adv.appendChild(advBody);
// assemble modal content
body.appendChild(desc);
body.appendChild(badges);
body.appendChild(capsWrap);
body.appendChild(adv); // advanced section at the very end
dialog.appendChild(header);
dialog.appendChild(body);
overlay.appendChild(dialog);
mount.appendChild(overlay);
// focus management (trap + restore)
const opener = options.trigger instanceof HTMLElement ? options.trigger : /** @type {HTMLElement|null} */(document.activeElement);
const focusableSelectors = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(',');
const findFocusable = () => /** @type {HTMLElement[]} */(Array.from(dialog.querySelectorAll(focusableSelectors))),
focusFirst = () => {
const items = findFocusable();
const target = items[0] || closeBtn;
target.focus();
};
const onKeydown = (e) => {
if (e.key === 'Tab') {
const items = findFocusable();
if (items.length === 0) {
e.preventDefault();
closeBtn.focus();
return;
}
const first = items[0],
last = items[items.length - 1],
active = /** @type {HTMLElement} */(document.activeElement);
if (!e.shiftKey && active === last) {
e.preventDefault();
first.focus();
} else if (e.shiftKey && active === first) {
e.preventDefault();
last.focus();
}
}
if (e.key === 'Escape') {
e.preventDefault();
close();
}
};
const onOverlayClick = (e) => {
if (e.target === overlay) close();
};
overlay.addEventListener('click', onOverlayClick);
document.addEventListener('keydown', onKeydown);
closeBtn.addEventListener('click', () => close());
// prevent background scroll while open
const prevOverflow = document.documentElement.style.overflow;
document.documentElement.style.overflow = 'hidden';
// open
setTimeout(focusFirst, 0);
function close() {
overlay.removeEventListener('click', onOverlayClick);
document.removeEventListener('keydown', onKeydown);
document.documentElement.style.overflow = prevOverflow;
overlay.remove();
if (opener && typeof opener.focus === 'function') opener.focus();
}
return { close, root: overlay };
}
// helpers
function formatBytes(n) {
if (typeof n !== 'number') return '—';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0, v = n;
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
return `${v.toFixed(1)} ${units[i]}`;
}
const formatEpoch = (epochSeconds) => {
if (!Number.isFinite(epochSeconds)) return '—';
try { return new Date(epochSeconds * 1000).toLocaleString(); } catch { return '—'; }
}
function formatIso(iso) {
if (!iso || typeof iso !== 'string') return '—';
const d = new Date(iso);
return isNaN(d.getTime()) ? iso : d.toLocaleString();
}
const safeText = (v) => { return v === undefined || v === null ? null : String(v); },
prettyJson = (obj) => {
try { return JSON.stringify(obj, null, 2); } catch { return String(obj); }
}
const makeOptionLabel = (m, models) => {
const base = m?.name ?? m?.id ?? 'model',
duplicates = models.filter(x => (x?.name ?? x?.id) === (m?.name ?? m?.id));
if (duplicates.length > 1) {
const hint = m?.ollama?.details?.parameter_size || m?.ollama?.details?.quantization_level;
return hint ? `${base}${hint}` : `${base}${String(m?.id).slice(0, 8)}`;
}
return base;
};
// render options
function renderModelList(models) {
const sel = document.querySelector('#model-select'),
card = document.querySelector('#selected-model-card');
if (!sel) {
console.warn('failed to find model dropdown');
return;
}
const list = Array.isArray(models) ? models : [];
sel.__modelsData = list;
if (card) card.__modelsData = list;
const prevValue = sel.value;
for (let i = sel.options.length - 1; i >= 1; i--) sel.remove(i);
list.forEach(m => {
const opt = document.createElement('option');
opt.value = String(m?.id ?? '');
opt.textContent = makeOptionLabel(m, list);
sel.appendChild(opt);
});
const selectedExists = list.some(m => String(m?.id ?? '') === prevValue);
sel.value = selectedExists ? prevValue : '';
const updateCard = (id) => {
if (!card) return;
const registry = card.__modelsData || [],
model = registry.find(m => String(m?.id ?? '') === id);
if (!id || !model) {
card.hidden = true;
card.dataset.modelId = '';
card.replaceChildren();
card.setAttribute('aria-label', 'No model selected');
return;
}
card.hidden = false;
card.dataset.modelId = id;
card.setAttribute('aria-label', `Selected model ${makeOptionLabel(model, registry)}`);
card.replaceChildren();
const title = document.createElement('span');
title.className = 'selected-model-title';
title.textContent = model?.name ?? model?.id ?? 'model';
const metaParts = [];
if (model?.owned_by) metaParts.push(model.owned_by);
const paramSize = model?.ollama?.details?.parameter_size;
if (paramSize) metaParts.push(paramSize);
const quant = model?.ollama?.details?.quantization_level;
if (quant) metaParts.push(quant);
const metaText = metaParts.join(' • '),
descRaw = (model?.info?.meta?.description ?? '').trim(),
descText = descRaw.length > 90 ? `${descRaw.slice(0, 87).trim()}` : descRaw;
const hint = document.createElement('span');
hint.className = 'selected-model-hint';
hint.textContent = 'Open model details';
card.appendChild(title);
if (metaText) {
const meta = document.createElement('span');
meta.className = 'selected-model-meta';
meta.textContent = metaText;
card.appendChild(meta);
}
if (descText) {
const desc = document.createElement('span');
desc.className = 'selected-model-desc';
desc.textContent = descText;
card.appendChild(desc);
}
card.appendChild(hint);
};
if (!sel.__modelChangeHandler) {
sel.__modelChangeHandler = (e) => {
const target = /** @type {HTMLSelectElement} */(e.currentTarget),
id = target.value;
if (id) updateCard(id);
// const registry = sel.__modelsData || [],
// model = registry.find(m => String(m?.id ?? '') === id);
// if (!model) return;
// generateModelCard(model, { trigger: sel });
};
sel.addEventListener('change', sel.__modelChangeHandler);
}
if (card && !card.dataset.bound) {
card.addEventListener('click', () => {
const id = card.dataset.modelId;
if (!id) return;
const registry = card.__modelsData || [];
const model = registry.find(m => String(m?.id ?? '') === id);
if (model) generateModelCard(model, { trigger: card });
});
card.dataset.bound = 'true';
}
updateCard(sel.value);
}