Model picker: group models by provider
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>
This commit is contained in:
@@ -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 =
|
||||
'<span class="mp-empty-title">Search ' + all.length + ' models</span>'
|
||||
+ '<span class="mp-empty-sub">Picks land in Recent · tap the dot to favorite</span>';
|
||||
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 =
|
||||
`<svg class="mp-provider-chevron${isCollapsed ? ' collapsed' : ''}" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`
|
||||
+ `<span class="mp-provider-name">${_providerDisplayName(provider)}</span>`
|
||||
+ `<span class="mp-provider-count">${models.length}</span>`;
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user