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