|
|
|
|
|
|
| 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 => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| const epOffline = !!item.offline;
|
| 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 || epOffline,
|
| staleReason: epOffline
|
| ? (item.ping_error || 'endpoint offline')
|
| : (isLocalDead ? (probeResult.error || 'not responding') : ''),
|
| offline: epOffline,
|
| });
|
| });
|
| });
|
| 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;
|
|
|
|
|
| nameSpan.title = m.display;
|
| row.appendChild(nameSpan);
|
|
|
|
|
|
|
|
|
| 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 favModels = favs.map(id => byId.get(id)).filter(Boolean);
|
| if (favModels.length) {
|
| _addSection('Favorites');
|
| favModels.forEach(m => { shown.add(m.mid); _addRow(m); });
|
| }
|
|
|
|
|
|
|
| if (all.length > BROWSE_ALL_LIMIT) {
|
| const recentModels = _loadRecent()
|
| .map(id => byId.get(id))
|
| .filter(Boolean)
|
| .filter(m => !shown.has(m.mid))
|
| .slice(0, RECENT_MAX);
|
| if (recentModels.length) {
|
| _addSection('Recent');
|
| recentModels.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';
|
|
|
|
|
| label.title = modelId || '';
|
| const logo = modelId ? providerLogo(modelId) : null;
|
| if (logo) {
|
| label.innerHTML = '<span class="model-picker-logo">' + logo + '</span> ' + displayName;
|
| } else {
|
| label.textContent = displayName;
|
| }
|
| }
|
|
|