| |
| |
|
|
| import { providerLogo } from './providers.js'; |
| import uiModule from './ui.js'; |
| import settingsModule from './settings.js'; |
| import { sortModelObjects } from './modelSort.js'; |
|
|
| const API_BASE = window.location.origin; |
|
|
| |
| |
| |
| |
| const RECENT_KEY = 'odysseus-model-recent'; |
| const FAVORITES_KEY = 'odysseus-model-favorites'; |
| const RECENT_MAX = 5; |
| |
| |
| const BROWSE_ALL_LIMIT = 12; |
|
|
| function _loadList(key) { |
| try { |
| const a = JSON.parse(localStorage.getItem(key) || '[]'); |
| return Array.isArray(a) ? a : []; |
| } catch { return []; } |
| } |
| function _saveList(key, list) { |
| try { localStorage.setItem(key, JSON.stringify(list)); } catch { } |
| } |
| function _loadRecent() { return _loadList(RECENT_KEY); } |
| function _pushRecent(mid) { |
| if (!mid) return; |
| const next = _loadRecent().filter(x => x !== mid); |
| next.unshift(mid); |
| _saveList(RECENT_KEY, next.slice(0, RECENT_MAX)); |
| } |
| function _loadFavorites() { return _loadList(FAVORITES_KEY); } |
| function _toggleFavorite(mid) { |
| const favs = _loadFavorites(); |
| const i = favs.indexOf(mid); |
| if (i >= 0) favs.splice(i, 1); |
| else favs.push(mid); |
| _saveList(FAVORITES_KEY, favs); |
| |
| try { |
| if (window.modelsModule && typeof window.modelsModule.refreshModels === 'function') { |
| window.modelsModule.refreshModels(); |
| } |
| } catch { } |
| return i < 0; |
| } |
|
|
| |
| function _handlePickerKeydown(e, listEl, itemSelector, closeFn) { |
| if (e.key === 'Escape') { closeFn(); return; } |
| if (e.key === 'Enter') { |
| e.preventDefault(); |
| const active = listEl.querySelector(itemSelector + '.kb-active') || listEl.querySelector(itemSelector); |
| if (active) active.click(); |
| return; |
| } |
| if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { |
| e.preventDefault(); |
| const items = [...listEl.querySelectorAll(itemSelector)].filter(el => el.style.display !== 'none'); |
| if (!items.length) return; |
| const cur = items.findIndex(el => el.classList.contains('kb-active')); |
| items.forEach(el => el.classList.remove('kb-active')); |
| let next; |
| if (e.key === 'ArrowDown') next = cur < items.length - 1 ? cur + 1 : 0; |
| else next = cur > 0 ? cur - 1 : items.length - 1; |
| items[next].classList.add('kb-active'); |
| items[next].scrollIntoView({ block: 'nearest' }); |
| } |
| } |
|
|
| |
| let _deps = null; |
| let _autoSelectingDefault = false; |
|
|
| function _modelExists(modelId, url) { |
| if (!modelId || !window.modelsModule || !window.modelsModule.getCachedItems) return false; |
| const items = window.modelsModule.getCachedItems() || []; |
| if (!items.length) return true; |
| const targetUrl = (url || '').replace(/\/+$/, ''); |
| return items.some(item => { |
| if (item.offline) return false; |
| const itemUrl = (item.url || '').replace(/\/+$/, ''); |
| const models = (item.models || []).concat(item.models_extra || []); |
| return models.includes(modelId) && (!targetUrl || itemUrl === targetUrl); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function initModelPicker(deps) { |
| _deps = deps; |
| _initModelPickerDropdown(); |
| } |
|
|
| function _initModelPickerDropdown() { |
| const wrap = document.getElementById('model-picker-wrap'); |
| const btn = document.getElementById('model-picker-btn'); |
| const menu = document.getElementById('model-picker-menu'); |
| const search = document.getElementById('model-picker-search'); |
| const listEl = document.getElementById('model-picker-list'); |
| const searchRow = menu ? menu.querySelector('.model-picker-search-row') : null; |
| if (!wrap || !btn || !menu || !search || !listEl) return; |
|
|
| function _close() { |
| if (menu.classList.contains('hidden')) return; |
| |
| const _scrollBtn = document.getElementById('scroll-bottom-btn'); |
| if (_scrollBtn) _scrollBtn.style.display = ''; |
| menu.classList.add('closing'); |
| menu.addEventListener('animationend', function _onDone() { |
| menu.removeEventListener('animationend', _onDone); |
| menu.classList.remove('closing'); |
| menu.classList.add('hidden'); |
| search.value = ''; |
| }, { once: true }); |
| |
| setTimeout(() => { |
| if (!menu.classList.contains('hidden')) { |
| menu.classList.remove('closing'); |
| menu.classList.add('hidden'); |
| search.value = ''; |
| } |
| }, 200); |
| } |
|
|
| function _openPickerShortcut(kind) { |
| _close(); |
| try { |
| if (kind === 'cookbook') { |
| if (window.cookbookModule && typeof window.cookbookModule.open === 'function') { |
| window.cookbookModule.open(); |
| } else { |
| const btn = document.getElementById('tool-cookbook-btn') || document.getElementById('rail-cookbook'); |
| if (btn) btn.click(); |
| else location.hash = '#cookbook'; |
| } |
| } else if (kind === 'settings') { |
| if (settingsModule && typeof settingsModule.open === 'function') settingsModule.open(); |
| } else if (window.adminModule && typeof window.adminModule.open === 'function') { |
| window.adminModule.open('services'); |
| } else if (settingsModule && typeof settingsModule.open === 'function') { |
| settingsModule.open('services'); |
| } |
| } catch (_) {} |
| } |
|
|
| |
| |
| |
| let _localProbe = {}; |
| let _localProbeFetchedAt = 0; |
| const _LOCAL_PROBE_TTL_MS = 5000; |
|
|
| async function _refreshLocalProbe() { |
| const now = Date.now(); |
| if (now - _localProbeFetchedAt < _LOCAL_PROBE_TTL_MS) return; |
| _localProbeFetchedAt = now; |
| try { |
| const r = await fetch('/api/model-endpoints/probe-local', { credentials: 'same-origin' }); |
| if (r.ok) _localProbe = (await r.json()) || {}; |
| } catch (_) { } |
| } |
|
|
| function _getAllModels() { |
| const items = (window.modelsModule && window.modelsModule.getCachedItems) ? window.modelsModule.getCachedItems() : []; |
| const result = []; |
| const seen = new Set(); |
| items.forEach(item => { |
| if (item.offline) return; |
| const allModels = (item.models || []).concat(item.models_extra || []); |
| const allDisplay = (item.models_display || []).concat(item.models_extra_display || []); |
| |
| const probeResult = item.endpoint_id ? _localProbe[item.endpoint_id] : null; |
| const isLocalDead = !!(probeResult && probeResult.alive === false); |
| allModels.forEach((mid, i) => { |
| |
| if (seen.has(mid)) return; |
| seen.add(mid); |
| result.push({ |
| mid, |
| display: (allDisplay[i] || mid).split('/').pop(), |
| url: item.url, |
| endpointId: item.endpoint_id, |
| epName: item.endpoint_name || '', |
| providerText: [ |
| item.endpoint_name || '', |
| item.category || '', |
| item.host || '', |
| item.url || '', |
| ].filter(Boolean).join(' '), |
| stale: isLocalDead, |
| staleReason: isLocalDead ? (probeResult.error || 'not responding') : '', |
| }); |
| }); |
| }); |
| return sortModelObjects(result); |
| } |
|
|
| |
| 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(); |
| const q = (filter || '').trim().toLowerCase(); |
| const hasAnyModel = all.length > 0; |
| listEl.classList.toggle('is-empty', !hasAnyModel); |
| menu.classList.toggle('no-models', !hasAnyModel); |
| if (search) { |
| search.placeholder = hasAnyModel ? 'Search models…' : 'No models connected'; |
| } |
| if (searchRow) { |
| searchRow.classList.toggle('searching', !!q); |
| } |
|
|
| if (!hasAnyModel) return; |
|
|
| |
| |
| const byId = new Map(); |
| all.forEach(m => { if (!byId.has(m.mid)) byId.set(m.mid, m); }); |
|
|
| const favs = _loadFavorites(); |
|
|
| function _addSection(label) { |
| const el = document.createElement('div'); |
| el.className = 'mp-section-label'; |
| el.textContent = label; |
| listEl.appendChild(el); |
| } |
| function _addEmpty(text) { |
| const empty = document.createElement('div'); |
| empty.className = 'model-switch-empty'; |
| empty.textContent = text; |
| listEl.appendChild(empty); |
| } |
| function _addRow(m) { |
| const row = document.createElement('div'); |
| row.className = 'model-switch-item'; |
| if (m.stale) { |
| row.classList.add('model-switch-stale'); |
| row.style.opacity = '0.45'; |
| row.title = `Local server appears offline: ${m.staleReason}. Click to try anyway, or relaunch in Cookbook.`; |
| } |
| const _mlogo = providerLogo(m.mid); |
| if (_mlogo) { |
| const logoSpan = document.createElement('span'); |
| logoSpan.className = 'provider-logo'; |
| logoSpan.style.opacity = '0.6'; |
| logoSpan.innerHTML = _mlogo; |
| row.appendChild(logoSpan); |
| } |
| const nameSpan = document.createElement('span'); |
| nameSpan.className = 'mp-model-name'; |
| nameSpan.textContent = m.display; |
| row.appendChild(nameSpan); |
| if (m.stale) { |
| const badge = document.createElement('span'); |
| badge.className = 'model-switch-stale-badge'; |
| badge.textContent = 'offline'; |
| badge.style.cssText = 'font-size:10px;opacity:0.7;padding:1px 6px;border:1px solid var(--border);border-radius:8px;margin-left:6px;'; |
| row.appendChild(badge); |
| } |
| const epSpan = document.createElement('span'); |
| epSpan.className = 'model-switch-ep'; |
| |
| const _epDisplay = m.epName && !m.display.toLowerCase().includes(m.epName.toLowerCase().split('/').pop()) ? m.epName : ''; |
| epSpan.textContent = _epDisplay; |
| row.appendChild(epSpan); |
|
|
| |
| const favDot = document.createElement('button'); |
| favDot.type = 'button'; |
| favDot.className = 'mp-fav-dot' + (favs.includes(m.mid) ? ' active' : ''); |
| favDot.textContent = '●'; |
| const _setFavState = (on) => { |
| favDot.classList.toggle('active', on); |
| favDot.title = on ? 'Remove from favorites' : 'Add to favorites'; |
| favDot.setAttribute('aria-label', on ? 'Remove from favorites' : 'Add to favorites'); |
| favDot.setAttribute('aria-pressed', on ? 'true' : 'false'); |
| }; |
| _setFavState(favs.includes(m.mid)); |
| favDot.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| const nowFav = _toggleFavorite(m.mid); |
| _setFavState(nowFav); |
| favDot.classList.remove('pulse'); |
| void favDot.offsetWidth; |
| favDot.classList.add('pulse'); |
| |
| const idx = favs.indexOf(m.mid); |
| if (nowFav && idx < 0) favs.push(m.mid); |
| else if (!nowFav && idx >= 0) favs.splice(idx, 1); |
| if (uiModule && uiModule.showToast) uiModule.showToast(nowFav ? 'Favorited' : 'Unfavorited'); |
| |
| |
| |
| if (!q) { |
| const st = listEl.scrollTop; |
| _populate(''); |
| listEl.scrollTop = st; |
| } |
| }); |
| row.appendChild(favDot); |
|
|
| row.addEventListener('click', () => _pick(m)); |
| listEl.appendChild(row); |
| } |
|
|
| |
| if (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; |
| } |
|
|
| |
| const shown = new Set(); |
| const recentModels = _loadRecent() |
| .map(id => byId.get(id)) |
| .filter(Boolean) |
| .slice(0, RECENT_MAX); |
| const favModels = favs.map(id => byId.get(id)).filter(Boolean); |
|
|
| if (recentModels.length) { |
| _addSection('Recent'); |
| recentModels.forEach(m => { shown.add(m.mid); _addRow(m); }); |
| } |
| if (favModels.length) { |
| _addSection('Favorites'); |
| favModels.forEach(m => { shown.add(m.mid); _addRow(m); }); |
| } |
|
|
| |
| if (all.length <= BROWSE_ALL_LIMIT) { |
| const rest = all.filter(m => !shown.has(m.mid)); |
| if (rest.length) { |
| if (shown.size) _addSection('All models'); |
| rest.forEach(_addRow); |
| } |
| } else { |
| |
| 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); |
| |
| group.appendChild(listEl.lastElementChild); |
| }); |
| listEl.appendChild(group); |
| if (_justExpandedProvider === provider) _justExpandedProvider = null; |
| } |
| }); |
| } |
| } |
|
|
| async function _pick(m) { |
| const currentSessionId = _deps.getCurrentSessionId(); |
| const _pendingChat = _deps.getPendingChat(); |
|
|
| |
| |
| if (m && m.mid) _pushRecent(m.mid); |
|
|
| |
| |
| try { document.dispatchEvent(new CustomEvent('odysseus:model-picked', { detail: m })); } catch {} |
|
|
| |
| if (document.activeElement) document.activeElement.blur(); |
| _close(); |
| |
| if (window.innerWidth >= 768) { |
| const _ta = document.getElementById('message'); |
| if (_ta) setTimeout(() => _ta.focus(), 50); |
| } |
| if (!currentSessionId && _pendingChat) { |
| |
| _deps.setPendingChat({ url: m.url, modelId: m.mid, endpointId: m.endpointId }); |
| |
| updateModelPicker(); |
| uiModule.showToast(`Using ${m.display}`); |
| return; |
| } else if (!currentSessionId) { |
| |
| await _deps.createDirectChat(m.url, m.mid, m.endpointId); |
| } else { |
| |
| const fd = new FormData(); |
| fd.append('model', m.mid); |
| fd.append('endpoint_url', m.url); |
| if (m.endpointId) fd.append('endpoint_id', m.endpointId); |
| try { |
| const res = await fetch(`${API_BASE}/api/session/${currentSessionId}`, { method: 'PATCH', body: fd }); |
| if (!res.ok) { |
| uiModule.showError('Failed to set model'); |
| return; |
| } |
| const sessions = _deps.getSessions(); |
| const s = sessions.find(x => x.id === currentSessionId); |
| if (s) { s.model = m.mid; s.endpoint_url = m.url; } |
| |
| } catch (e) { |
| uiModule.showError('Failed to set model: ' + e); |
| return; |
| } |
| } |
| |
| updateModelPicker(); |
| uiModule.showToast(`Using ${m.display}`); |
| } |
|
|
| document.addEventListener('odysseus:auto-select-model', async (e) => { |
| const detail = (e && e.detail) || {}; |
| const currentSessionId = _deps.getCurrentSessionId(); |
| const sessions = _deps.getSessions(); |
| const current = sessions.find(x => x.id === currentSessionId); |
| const pending = _deps.getPendingChat(); |
| if ((current && current.model) || (pending && pending.modelId)) return; |
|
|
| if (window.modelsModule && window.modelsModule.refreshModels) { |
| try { await window.modelsModule.refreshModels(true); } catch (_) {} |
| } |
| const items = window.modelsModule && window.modelsModule.getCachedItems ? window.modelsModule.getCachedItems() : []; |
| const targetEndpointId = detail.endpointId ? String(detail.endpointId) : ''; |
| const targetModel = detail.modelId || ''; |
| let match = null; |
| for (const item of items) { |
| if (item.offline) continue; |
| if (targetEndpointId && String(item.endpoint_id || '') !== targetEndpointId) continue; |
| const models = (item.models || []).concat(item.models_extra || []); |
| const displays = (item.models_display || []).concat(item.models_extra_display || []); |
| const idx = targetModel ? models.indexOf(targetModel) : (models.length ? 0 : -1); |
| if (idx >= 0) { |
| match = { |
| mid: models[idx], |
| display: (displays[idx] || models[idx]).split('/').pop(), |
| url: item.url || detail.url || '', |
| endpointId: item.endpoint_id || detail.endpointId || '', |
| epName: item.endpoint_name || detail.endpointName || '', |
| providerText: [item.endpoint_name || detail.endpointName || '', item.url || detail.url || ''].filter(Boolean).join(' '), |
| }; |
| break; |
| } |
| } |
| if (!match && detail.modelId && detail.url) { |
| match = { |
| mid: detail.modelId, |
| display: String(detail.modelId).split('/').pop(), |
| url: detail.url, |
| endpointId: detail.endpointId || '', |
| epName: detail.endpointName || '', |
| providerText: [detail.endpointName || '', detail.url || ''].filter(Boolean).join(' '), |
| }; |
| } |
| if (match) await _pick(match); |
| }); |
|
|
| btn.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| if (menu.classList.contains('hidden') || menu.classList.contains('closing')) { |
| |
| menu.classList.remove('closing', 'hidden'); |
| _populate(''); |
| if (window.modelsModule && window.modelsModule.refreshModels) { |
| window.modelsModule.refreshModels().then(() => { |
| if (!menu.classList.contains('hidden')) _populate(search.value || ''); |
| updateModelPicker(); |
| }).catch(() => {}); |
| } |
| |
| |
| |
| _refreshLocalProbe().then(() => { |
| if (!menu.classList.contains('hidden')) _populate(search.value || ''); |
| }); |
| if (window.innerWidth >= 768) search.focus(); |
| |
| const _scrollBtn = document.getElementById('scroll-bottom-btn'); |
| if (_scrollBtn) _scrollBtn.style.display = 'none'; |
| } else { |
| _close(); |
| } |
| }); |
|
|
| search.addEventListener('input', () => _populate(search.value)); |
| search.addEventListener('click', (e) => e.stopPropagation()); |
| search.addEventListener('keydown', (e) => { |
| _handlePickerKeydown(e, listEl, '.model-switch-item', _close); |
| }); |
| const addModelsBtn = document.getElementById('model-picker-add-models-btn'); |
| if (addModelsBtn) { |
| addModelsBtn.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| _openPickerShortcut('models'); |
| }); |
| } |
| document.addEventListener('click', (e) => { |
| if (!menu.classList.contains('hidden') && !menu.contains(e.target) && e.target !== btn) { |
| _close(); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| export function updateModelPicker() { |
| if (!_deps) return; |
| const label = document.getElementById('model-picker-label'); |
| if (!label) return; |
| |
| const wrap = document.getElementById('model-picker-wrap'); |
| if (window.groupModule && window.groupModule.isActive()) { |
| if (wrap) { wrap.style.display = 'none'; } |
| return; |
| } |
| |
| if (wrap) { |
| wrap.style.display = ''; |
| wrap.style.opacity = ''; |
| wrap.style.pointerEvents = ''; |
| } |
| const currentSessionId = _deps.getCurrentSessionId(); |
| const sessions = _deps.getSessions(); |
| const _pendingChat = _deps.getPendingChat(); |
| const s = sessions.find(x => x.id === currentSessionId); |
| let modelId = null; |
| if (s && s.model) { |
| modelId = s.model; |
| if (!_modelExists(modelId, s.endpoint_url || '')) { |
| modelId = null; |
| } |
| } else if (_pendingChat && _pendingChat.modelId) { |
| modelId = _pendingChat.modelId; |
| if (!_modelExists(modelId, _pendingChat.url || '')) { |
| _deps.setPendingChat(null); |
| modelId = null; |
| } |
| } |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| if (modelId && !currentSessionId && _pendingChat && window.modelsModule && window.modelsModule.getCachedItems) { |
| const items = window.modelsModule.getCachedItems(); |
| const allAvailable = []; |
| items.forEach(item => { |
| if (item.offline) return; |
| (item.models || []).concat(item.models_extra || []).forEach(m => allAvailable.push(m)); |
| }); |
| if (allAvailable.length > 0 && !allAvailable.includes(modelId)) { |
| |
| const fallback = items.find(item => !item.offline && (item.models || []).length > 0); |
| if (fallback) { |
| modelId = fallback.models[0]; |
| _deps.setPendingChat({ url: fallback.url, modelId, endpointId: fallback.endpoint_id }); |
| } |
| } |
| } |
| if (!modelId && !_autoSelectingDefault && window.modelsModule && window.modelsModule.getCachedItems) { |
| const items = window.modelsModule.getCachedItems(); |
| const first = items.find(item => !item.offline && ((item.models || []).length || (item.models_extra || []).length)); |
| if (first) { |
| const models = (first.models || []).concat(first.models_extra || []); |
| modelId = models[0]; |
| if (!currentSessionId) { |
| _deps.setPendingChat({ url: first.url, modelId, endpointId: first.endpoint_id }); |
| } else { |
| if (s) { s.model = modelId; s.endpoint_url = first.url; } |
| _autoSelectingDefault = true; |
| const fd = new FormData(); |
| fd.append('model', modelId); |
| fd.append('endpoint_url', first.url || ''); |
| if (first.endpoint_id) fd.append('endpoint_id', first.endpoint_id); |
| fetch(`${API_BASE}/api/session/${currentSessionId}`, { method: 'PATCH', body: fd }) |
| .catch(() => {}) |
| .finally(() => { _autoSelectingDefault = false; }); |
| } |
| } |
| } |
|
|
| const displayName = modelId ? modelId.split('/').pop() : 'Select model'; |
| const logo = modelId ? providerLogo(modelId) : null; |
| if (logo) { |
| label.innerHTML = '<span class="model-picker-logo">' + logo + '</span> ' + displayName; |
| } else { |
| label.textContent = displayName; |
| } |
| } |
|
|