From 5268a546bc615856875a28816df95043243e02e6 Mon Sep 17 00:00:00 2001 From: danielxb Date: Tue, 2 Jun 2026 14:14:22 +1000 Subject: [PATCH] Model picker: group models by provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebased on current main. Integrates with the new Recent/Favorites system — provider groups appear below Recent and Favorites in browse mode for large catalogs (>12 models). Changes: - Models grouped by canonical provider with collapsible sections - Chevron animation consistent with sidebar sections - Domino cascade on expand (only on just-opened group) - Provider display names (deepseek-ai -> DeepSeek, meta -> Llama, etc.) - Alias merging (meta + meta-llama -> one Llama group) - Search includes provider display names for filtering - Collapsed state persists in localStorage - No screenshot binary committed Co-authored-by: danielxb <5981902+danielxb@users.noreply.github.com> --- static/js/modelPicker.js | 116 ++++++++++++++++++++++++++++++++++----- static/style.css | 49 +++++++++++++++++ 2 files changed, 150 insertions(+), 15 deletions(-) diff --git a/static/js/modelPicker.js b/static/js/modelPicker.js index 96e7c54..7e5f848 100644 --- a/static/js/modelPicker.js +++ b/static/js/modelPicker.js @@ -209,6 +209,54 @@ function _initModelPickerDropdown() { return sortModelObjects(result); } + // ── Provider display names and grouping ── + const _PROVIDER_NAMES = { + '01-ai': 'Yi', 'abacusai': 'Abacus AI', 'adept': 'Adept', + 'ai21': 'AI21 Labs', 'ai21labs': 'AI21 Labs', 'aion-labs': 'Aion Labs', + 'aisingapore': 'AI Singapore', 'allenai': 'Allen AI', 'amazon': 'Amazon', + 'anthracite-org': 'Anthracite', 'anthropic': 'Anthropic', 'arcee-ai': 'Arcee AI', + 'baai': 'BAAI', 'baidu': 'Baidu', 'bigcode': 'BigCode', + 'black-forest-labs': 'Black Forest Labs', 'bytedance': 'ByteDance', + 'bytedance-seed': 'ByteDance', 'cognitivecomputations': 'Cognitive Computations', + 'cohere': 'Cohere', 'databricks': 'Databricks', 'deepcogito': 'DeepCogito', + 'deepseek': 'DeepSeek', 'deepseek-ai': 'DeepSeek', 'essentialai': 'Essential AI', + 'google': 'Google', 'gryphe': 'Gryphe', 'ibm': 'IBM', + 'ibm-granite': 'IBM Granite', 'inception': 'Inception', + 'inclusionai': 'Inclusion AI', 'inflection': 'Inflection', + 'kwaipilot': 'KwaiPilot', 'liquid': 'Liquid AI', 'mancer': 'Mancer', + 'meta': 'Llama', 'meta-llama': 'Llama', 'microsoft': 'Microsoft', + 'minimax': 'MiniMax', 'minimaxai': 'MiniMax', 'mistralai': 'Mistral', + 'moonshotai': 'Moonshot', 'morph': 'Morph', 'nex-agi': 'Nex AGI', + 'nousresearch': 'Nous Research', 'nv-mistralai': 'NVIDIA x Mistral', + 'nvidia': 'NVIDIA', 'openai': 'OpenAI', 'openrouter': 'OpenRouter', + 'perceptron': 'Perceptron', 'perplexity': 'Perplexity', 'poolside': 'Poolside', + 'prime-intellect': 'Prime Intellect', 'qwen': 'Qwen', 'rekaai': 'Reka', + 'relace': 'Relace', 'sao10k': 'Sao10k', 'sarvamai': 'Sarvam AI', + 'snowflake': 'Snowflake', 'stepfun': 'StepFun', 'stepfun-ai': 'StepFun', + 'stockmark': 'Stockmark', 'switchpoint': 'SwitchPoint', 'tencent': 'Tencent', + 'thedrummer': 'TheDrummer', 'undi95': 'Undi95', 'upstage': 'Upstage', + 'writer': 'Writer', 'x-ai': 'xAI', 'xiaomi': 'Xiaomi', + 'z-ai': 'Zhipu', 'zyphra': 'Zyphra', + '~anthropic': 'Anthropic', '~google': 'Google', + '~moonshotai': 'Moonshot', '~openai': 'OpenAI', + }; + const _PROVIDER_ALIAS = { + 'meta-llama': 'meta', 'deepseek': 'deepseek-ai', 'minimaxai': 'minimax', + 'stepfun-ai': 'stepfun', 'ai21labs': 'ai21', 'ibm-granite': 'ibm', + 'bytedance-seed': 'bytedance', '~anthropic': 'anthropic', + '~google': 'google', '~moonshotai': 'moonshotai', '~openai': 'openai', + }; + function _providerDisplayName(slug) { + return _PROVIDER_NAMES[slug] || slug.charAt(0).toUpperCase() + slug.slice(1).replace(/-/g, ' '); + } + function _providerSlug(mid) { + const slash = mid.indexOf('/'); + let slug = slash > 0 ? mid.substring(0, slash) : 'other'; + return _PROVIDER_ALIAS[slug] || slug; + } + const _collapsedProviders = new Set(_loadList('odysseus-model-collapsed')); + let _justExpandedProvider = null; + function _populate(filter) { listEl.innerHTML = ''; const all = _getAllModels(); @@ -319,13 +367,11 @@ function _initModelPickerDropdown() { // ── Search mode: flat, filtered results across the whole catalog ── if (q) { - const matches = all.filter(m => - [ - m.mid, - m.display, - m.epName, - m.providerText, - ].filter(Boolean).join(' ').toLowerCase().includes(q)); + const matches = all.filter(m => { + const provName = _providerDisplayName(_providerSlug(m.mid)).toLowerCase(); + return [m.mid, m.display, m.epName, m.providerText, provName] + .filter(Boolean).join(' ').toLowerCase().includes(q); + }); if (matches.length === 0) _addEmpty('No matching models'); else matches.forEach(_addRow); return; @@ -355,14 +401,54 @@ function _initModelPickerDropdown() { if (shown.size) _addSection('All models'); rest.forEach(_addRow); } - } else if (!recentModels.length && !favModels.length) { - // Large catalog, nothing pinned yet — point them at the search box. - const hint = document.createElement('div'); - hint.className = 'model-switch-empty mp-empty-hint'; - hint.innerHTML = - 'Search ' + all.length + ' models' - + 'Picks land in Recent · tap the dot to favorite'; - listEl.appendChild(hint); + } else { + // Large catalog: show provider groups with collapsible sections. + const rest = all.filter(m => !shown.has(m.mid)); + const groups = new Map(); + rest.forEach(m => { + const slug = _providerSlug(m.mid); + if (!groups.has(slug)) groups.set(slug, []); + groups.get(slug).push(m); + }); + const sorted = [...groups.keys()].sort((a, b) => + _providerDisplayName(a).localeCompare(_providerDisplayName(b))); + + sorted.forEach(provider => { + const models = groups.get(provider); + const isCollapsed = _collapsedProviders.has(provider); + const header = document.createElement('div'); + header.className = 'mp-provider-header'; + header.innerHTML = + `` + + `${_providerDisplayName(provider)}` + + `${models.length}`; + header.addEventListener('click', (e) => { + e.stopPropagation(); + if (_collapsedProviders.has(provider)) { + _collapsedProviders.delete(provider); + _justExpandedProvider = provider; + } else { + _collapsedProviders.add(provider); + _justExpandedProvider = null; + } + _saveList('odysseus-model-collapsed', [..._collapsedProviders]); + const st = listEl.scrollTop; + _populate(''); + listEl.scrollTop = st; + }); + listEl.appendChild(header); + if (!isCollapsed) { + const group = document.createElement('div'); + group.className = 'mp-provider-group' + (_justExpandedProvider === provider ? ' mp-just-expanded' : ''); + models.forEach(m => { + _addRow(m); + // Move the just-appended row into the group container + group.appendChild(listEl.lastElementChild); + }); + listEl.appendChild(group); + if (_justExpandedProvider === provider) _justExpandedProvider = null; + } + }); } } diff --git a/static/style.css b/static/style.css index a20127b..dcda5bf 100644 --- a/static/style.css +++ b/static/style.css @@ -2803,6 +2803,55 @@ body.bg-pattern-sparkles { font-size: 0.92em; opacity: 0.7; } + /* Provider group headers */ + .model-picker-list .mp-provider-header { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 8px; + cursor: pointer; + font-size: 0.78em; + font-weight: 500; + color: var(--fg); + border-radius: 4px; + user-select: none; + } + .model-picker-list .mp-provider-header:hover { + background: color-mix(in srgb, var(--fg) 6%, transparent); + } + .model-picker-list .mp-provider-chevron { + display: inline-flex; + opacity: 0.4; + transition: transform 0.2s, opacity 0.15s; + flex-shrink: 0; + } + .model-picker-list .mp-provider-header:hover .mp-provider-chevron { + opacity: 0.7; + } + .model-picker-list .mp-provider-chevron.collapsed { + transform: rotate(-90deg); + } + .model-picker-list .mp-provider-name { flex: 1; } + .model-picker-list .mp-provider-count { font-size: 0.85em; opacity: 0.4; } + /* Domino expand (15% faster than sidebar) */ + .mp-provider-group.mp-just-expanded .model-switch-item { + animation: mp-domino-in 0.31s cubic-bezier(0.22, 1.61, 0.36, 1) backwards; + } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(1) { animation-delay: 0.035s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(2) { animation-delay: 0.07s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(3) { animation-delay: 0.105s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(4) { animation-delay: 0.14s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(5) { animation-delay: 0.175s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(6) { animation-delay: 0.21s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(7) { animation-delay: 0.245s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(8) { animation-delay: 0.28s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(9) { animation-delay: 0.315s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(10) { animation-delay: 0.35s; } + @keyframes mp-domino-in { + 0% { opacity: 0; transform: translateY(6px) scale(0.94); } + 60% { opacity: 1; } + 100% { opacity: 1; transform: translateY(0) scale(1); } + } /* Comfortable touch targets on phones / narrow screens. */ @media (hover: none) and (pointer: coarse), (max-width: 768px) { .model-picker-list .model-switch-item {