// static/js/settings.js — Settings panel module (ES6) // User-facing preferences: AI models, search, appearance import uiModule from './ui.js?v=20260916largetoolscroll1'; import searchModule from './search.js'; import { byId } from './settings/dom.js'; import { postSettings as _postSettings } from './settings/api.js'; import { initTtsSettings, initSttSettings } from './settings/speech.js'; import { initDocumentWritingStyle } from './settings/writingStyle.js'; import { initImageSettings } from './settings/imageModels.js'; import { initAgentSettings } from './settings/agent.js'; import { getSettingsRegistryIssues } from './settings/registry.js'; import { bindSettingsSearch } from './settings/search.js'; import { bindSettingsSidebar } from './settings/sidebar.js'; import { bindSettingsNavigation } from './settings/navigation.js'; import { bindSettingsDrag, bindSettingsClose, bindOpenPromptModalLink, } from './settings/lifecycle.js'; import { bindSettingsPeekToggle } from './settings/peek.js'; import { createSettingsShell } from './settings/shell.js'; import { handleSettingsOauthReturn } from './settings/oauthReturn.js'; import { sortModelIds } from './modelSort.js'; import { providerLogo } from './providers.js'; import { isAltGrEvent } from './platform.js'; import { bindMenuDismiss } from './escMenuStack.js'; import { invalidateSettings } from './appConfig.js'; import { SEARCH_PROVIDER_LOGOS as _SEARCH_PROVIDER_LOGOS } from './searchProviderIcons.js'; let initialized = false; let modalEl = null; let _authPolicy = { password_min_length: 8 }; /** * POST a settings patch, then drop the shared snapshot in appConfig.js. * * Every write in this file goes through here so no save path can forget the * invalidation — a stale settings object served for the rest of the session is * a worse bug than the duplicate fetches the cache removes. The invalidation is * in a `finally` because a request that throws on the way back may still have * been applied server-side. * * Reads in this file deliberately stay direct fetches: this panel is the writer * and edits what it reads, so it must see the authoritative state, not a cache. */ const el = byId; function esc(s) { return uiModule.esc(s); } function safeRasterDataUrl(raw) { const value = String(raw || '').trim(); return /^data:image\/(?:png|jpe?g|gif|webp);base64,[a-z0-9+/=\s]+$/i.test(value) ? value : ''; } /* ── Settings shell coordination ── Panel activation side effects, the admin handoff, admin-only visibility and the public open/close API live in ./settings/shell.js. This coordinator only supplies the panel-owned pieces the shell needs. */ const _shell = createSettingsShell({ getModal: () => modalEl, ensureInitialized: () => { if (!initialized) initAll(); }, syncAppearanceCheckboxes: () => syncAppearanceCheckboxes(), refreshAiModelEndpoints: () => refreshAiModelEndpoints(), isAdmin: () => !!window._isAdmin, getAdminModule: () => window.adminModule, }); /* ═══════════════════════════════════════════ AI TAB ═══════════════════════════════════════════ */ const _aiEndpointRefreshers = new Set(); let _aiEndpointRefreshInFlight = null; async function _fetchModelEndpoints() { const epRes = await fetch('/api/model-endpoints', { credentials: 'same-origin' }); if (!epRes.ok) throw new Error(`HTTP ${epRes.status}`); const endpoints = await epRes.json(); return Array.isArray(endpoints) ? endpoints : []; } function _selectableEndpointModels(endpoint) { if (!endpoint) return []; return Array.from(new Set([ ...(Array.isArray(endpoint.models) ? endpoint.models : []), ...(Array.isArray(endpoint.pinned_models) ? endpoint.pinned_models : []), ].filter(Boolean).map(String))); } function _endpointLabel(ep) { return ep.name + (ep.online ? '' : ' (offline)'); } function _fillEndpointSelect(selectEl, endpoints, selected, keepBlank) { if (!selectEl) return; const previous = selected !== undefined ? selected : selectEl.value; const blankText = keepBlank && selectEl.options[0] && selectEl.options[0].value === '' ? selectEl.options[0].textContent : null; while (selectEl.options.length) selectEl.remove(0); if (blankText !== null) { const blank = document.createElement('option'); blank.value = ''; blank.textContent = blankText; selectEl.appendChild(blank); } (endpoints || []).forEach(function(ep) { if (!ep.is_enabled) return; const opt = document.createElement('option'); opt.value = ep.id; opt.textContent = _endpointLabel(ep); selectEl.appendChild(opt); }); if (previous && Array.from(selectEl.options).some(function(o) { return o.value === previous; })) { selectEl.value = previous; } else if (blankText !== null) { selectEl.value = ''; } _syncEndpointLogo(selectEl); } // Mirror the selected model's provider logo into a sibling