diff --git a/js/provider-model-controls.js b/js/provider-model-controls.js new file mode 100644 index 00000000..cb9d7ba3 --- /dev/null +++ b/js/provider-model-controls.js @@ -0,0 +1,200 @@ +// provider-model-controls.js - provider model dropdowns, pricing, and custom model selection. + +import { escapeHTML, showNotification } from './utils.js'; +import { + fetchOpenRouterModelPricing, + getCustomApiModel, + getOpenRouterModel, + getPpqModel, + getRoutstrModel, + getVeniceE2EE, + getVeniceModel, + renderModelPricingHint, + setCustomApiModel, + setOpenRouterModel, + setPpqModel, + setRoutstrModel, + setVeniceE2EE, + setVeniceModel, +} from './api.js'; +import { buildModelOptions } from './provider-panel-renderers.js'; + +export function updateVeniceModelPricing(modelId) { + const el = document.getElementById('venice-model-pricing'); + if (el) el.innerHTML = renderModelPricingHint('venice', modelId || getVeniceModel()); +} + +export function renderVeniceModelDropdown(models) { + const area = document.getElementById('venice-model-area'); + if (!area || !models.length) return; + const currentModel = getVeniceModel(); + const opts = buildModelOptions('venice', models, currentModel, function(m) { return m.name || m.id; }); + area.innerHTML = '' + + '' + + '
' + renderModelPricingHint('venice', currentModel) + '
'; +} + +export function onVeniceModelDropdownChange(value) { + const previous = getVeniceModel(); + setVeniceModel(value); + localStorage.setItem(getVeniceE2EE() ? 'labcharts-venice-model-e2ee' : 'labcharts-venice-model-regular', value); + if (previous !== value) window.clearE2EESession?.(); + updateVeniceModelPricing(value); +} + +export function toggleVeniceE2EE(on) { + setVeniceE2EE(on); + if (!on) window.clearE2EESession?.(); + // Swap model dropdown to E2EE or regular model list. + const listKey = on ? 'labcharts-venice-e2ee-models' : 'labcharts-venice-models'; + let models = []; try { models = JSON.parse(localStorage.getItem(listKey) || '[]'); } catch {} + if (models.length) { + const prevKey = on ? 'labcharts-venice-model-regular' : 'labcharts-venice-model-e2ee'; + const restoreKey = on ? 'labcharts-venice-model-e2ee' : 'labcharts-venice-model-regular'; + localStorage.setItem(prevKey, getVeniceModel()); + const restored = localStorage.getItem(restoreKey); + const newModel = restored && models.some(m => m.id === restored) ? restored : models[0].id; + setVeniceModel(newModel); + renderVeniceModelDropdown(models); + } + const el = document.getElementById('venice-e2ee-indicator'); + if (el) el.style.display = on ? '' : 'none'; + window.updateChatHeaderModel?.(); + window.refreshWebSearchToggle?.(); +} + +export function updateOpenRouterModelPricing(modelId) { + const el = document.getElementById('openrouter-model-pricing'); + if (el) el.innerHTML = renderModelPricingHint('openrouter', modelId || getOpenRouterModel()); +} + +export function renderOpenRouterModelDropdown(models) { + const area = document.getElementById('openrouter-model-area'); + if (!area || !models.length) return; + const currentModel = getOpenRouterModel(); + const isCustom = !models.some(m => m.id === currentModel); + const opts = buildModelOptions('openrouter', models, currentModel, function(m) { return m.name || m.id; }); + area.innerHTML = '' + + '' + + '
' + + 'Press Enter to apply — checks model connectivity' + + '
' + renderModelPricingHint('openrouter', currentModel) + '
'; +} + +export async function applyCustomOpenRouterModel(modelId) { + const id = modelId.trim(); + if (!id) return; + setOpenRouterModel(id); + const pricingEl = document.getElementById('openrouter-model-pricing'); + if (pricingEl) pricingEl.innerHTML = 'Checking pricing\u2026'; + const select = document.getElementById('openrouter-model-select'); + const input = document.getElementById('openrouter-custom-model'); + const inDropdown = select && [...select.options].some(o => o.value === id); + if (select) { + if (inDropdown) { + select.value = id; + if (input) { input.value = ''; input.style.borderColor = ''; } + } else { + let customOpt = select.querySelector('option[value="__custom"]'); + if (!customOpt) { + customOpt = document.createElement('option'); + customOpt.value = '__custom'; + customOpt.disabled = true; + customOpt.textContent = 'Using custom model'; + select.insertBefore(customOpt, select.firstChild); + } + customOpt.selected = true; + } + } + const indicator = document.getElementById('openrouter-model-health'); + if (indicator) { indicator.textContent = '\u23f3'; indicator.title = 'Checking...'; indicator.style.color = 'var(--text-muted)'; } + try { + await window.callClaudeAPI({ messages: [{ role: 'user', content: 'hi' }], maxTokens: 1 }); + if (indicator) { indicator.textContent = '\u2713'; indicator.title = 'Model responding'; indicator.style.color = 'var(--green)'; } + if (input && !inDropdown) input.style.borderColor = 'var(--green)'; + showNotification('Model set: ' + id, 'info'); + await fetchOpenRouterModelPricing(id); + updateOpenRouterModelPricing(id); + } catch (e) { + if (indicator) { indicator.textContent = '\u2717'; indicator.title = e.message || 'Connection failed'; indicator.style.color = 'var(--red)'; } + if (input) input.style.borderColor = 'var(--red)'; + updateOpenRouterModelPricing(id); + showNotification('Model check failed: ' + (e.message || 'unknown error'), 'error'); + } +} + +export function onOpenRouterDropdownChange(value) { + setOpenRouterModel(value); + updateOpenRouterModelPricing(value); + const input = document.getElementById('openrouter-custom-model'); + if (input) { input.value = ''; input.style.borderColor = ''; } + const health = document.getElementById('openrouter-model-health'); + if (health) { health.textContent = ''; health.title = ''; } + const select = document.getElementById('openrouter-model-select'); + const customOpt = select?.querySelector('option[value="__custom"]'); + if (customOpt) customOpt.remove(); +} + +export function updateRoutstrModelPricing(modelId) { + const el = document.getElementById('routstr-model-pricing'); + if (el) el.innerHTML = renderModelPricingHint('routstr', modelId || getRoutstrModel()); +} + +export function renderRoutstrModelDropdown(models) { + const area = document.getElementById('routstr-model-area'); + if (!area || !models.length) return; + let currentModel = getRoutstrModel(); + const modelIds = models.map(m => m.id); + if (currentModel && !modelIds.includes(currentModel)) { + currentModel = modelIds[0]; + setRoutstrModel(currentModel); + } + const opts = buildModelOptions('routstr', models, currentModel, function(m) { return m.name || m.id; }); + area.innerHTML = '' + + '' + + '
' + renderModelPricingHint('routstr', currentModel) + '
'; +} + +export function renderPpqModelDropdown(models) { + const area = document.getElementById('ppq-model-area'); + if (!area || !models.length) return; + const currentModel = getPpqModel(); + const opts = buildModelOptions('ppq', models, currentModel, function(m) { return m.name || m.id; }); + area.innerHTML = '' + + '' + + '
' + renderModelPricingHint('ppq', currentModel) + '
'; +} + +export function updatePpqModelPricing(modelId) { + const el = document.getElementById('ppq-model-pricing'); + if (el) el.innerHTML = renderModelPricingHint('ppq', modelId); +} + +export function renderCustomApiModelDropdown(models) { + const area = document.getElementById('custom-model-area'); + if (!area) return; + const currentModel = getCustomApiModel(); + const opts = buildModelOptions('custom', models, currentModel, function(m) { return m.name || m.id; }); + const isCustom = !models.some(m => m.id === currentModel) && currentModel; + area.innerHTML = ` + +
+
${renderModelPricingHint('custom', currentModel)}
`; +} + +export function updateCustomModelPricing(modelId) { + const el = document.getElementById('custom-model-pricing'); + if (el) el.innerHTML = renderModelPricingHint('custom', modelId || getCustomApiModel()); +} + +export function applyCustomApiManualModel() { + const input = document.getElementById('custom-manual-model'); + if (!input) return; + const model = input.value.trim(); + if (!model) { showNotification('Enter a model ID', 'error'); return; } + setCustomApiModel(model); + const select = document.getElementById('custom-model-select'); + if (select) select.value = model; + updateCustomModelPricing(model); + showNotification('Model set to ' + model, 'success'); +} diff --git a/js/provider-panels.js b/js/provider-panels.js index b3b4a118..9638094b 100644 --- a/js/provider-panels.js +++ b/js/provider-panels.js @@ -1,21 +1,19 @@ -// provider-panels.js — AI provider settings behavior, model dropdowns, balance display, key validation +// provider-panels.js - AI provider settings behavior, balance display, key validation, and wallet flows import { escapeHTML, escapeAttr, showNotification, showConfirmDialog } from './utils.js'; import { getVeniceKey, saveVeniceKey, getOpenRouterKey, saveOpenRouterKey, getAIProvider, setAIProvider, - getVeniceModel, setVeniceModel, getOpenRouterModel, setOpenRouterModel, getOllamaMainModel, setOllamaMainModel, getOllamaPIIModel, setOllamaPIIModel, getOllamaPIIUrl, setOllamaPIIUrl, validateVeniceKey, validateOpenRouterKey, fetchVeniceModels, fetchOpenRouterModels, - renderModelPricingHint, - fetchOpenRouterModelPricing, getOpenRouterBalance, getVeniceBalance, - getRoutstrKey, saveRoutstrKey, getRoutstrModel, setRoutstrModel, + getOpenRouterBalance, getVeniceBalance, + getRoutstrKey, saveRoutstrKey, fetchRoutstrModels, validateRoutstrKey, createRoutstrAccount, setAIPaused, - getVeniceE2EE, setVeniceE2EE, + getVeniceE2EE, getCustomApiUrl, setCustomApiUrl, getCustomApiKey, saveCustomApiKey, - getCustomApiModel, setCustomApiModel, fetchCustomApiModels, validateCustomApiKey, - getPpqKey, savePpqKey, getPpqModel, setPpqModel, + fetchCustomApiModels, validateCustomApiKey, + getPpqKey, savePpqKey, fetchPpqModels, validatePpqKey, createPpqAccount, getPpqBalance, savePpqCreditId, createPpqTopup, checkPpqTopupStatus, rememberOpenRouterOAuthPreviousProvider, clearOpenRouterOAuthSession @@ -24,7 +22,24 @@ import { getOllamaConfig, checkOllama, checkOpenAICompatible, saveOllamaConfig, import { detectHardware, assessModel, assessFitness, getBestModel, getUpgradeSuggestion, saveHardwareOverride, getHardwareOverride } from './hardware.js'; import { updateKeyCache, encryptedSetItem } from './crypto.js'; import { ensureQRCode } from './provider-qr.js'; -import { renderAIProviderPanel, buildModelOptions } from './provider-panel-renderers.js'; +import { renderAIProviderPanel } from './provider-panel-renderers.js'; +import { + applyCustomApiManualModel, + applyCustomOpenRouterModel, + onOpenRouterDropdownChange, + onVeniceModelDropdownChange, + renderCustomApiModelDropdown, + renderOpenRouterModelDropdown, + renderPpqModelDropdown, + renderRoutstrModelDropdown, + renderVeniceModelDropdown, + toggleVeniceE2EE, + updateCustomModelPricing, + updateOpenRouterModelPricing, + updatePpqModelPricing, + updateRoutstrModelPricing, + updateVeniceModelPricing, +} from './provider-model-controls.js'; import { configureRoutstrWalletPanels, clearRoutstrWalletTimers, @@ -55,6 +70,23 @@ import { } from './provider-wallet-panels.js'; export { renderAIProviderPanel } from './provider-panel-renderers.js'; +export { + applyCustomApiManualModel, + applyCustomOpenRouterModel, + onOpenRouterDropdownChange, + onVeniceModelDropdownChange, + renderCustomApiModelDropdown, + renderOpenRouterModelDropdown, + renderPpqModelDropdown, + renderRoutstrModelDropdown, + renderVeniceModelDropdown, + toggleVeniceE2EE, + updateCustomModelPricing, + updateOpenRouterModelPricing, + updatePpqModelPricing, + updateRoutstrModelPricing, + updateVeniceModelPricing, +} from './provider-model-controls.js'; export { refreshCashuWalletBalance, @@ -563,10 +595,6 @@ export function refreshVeniceBalance() { else if (el) el.textContent = 'Balance: unavailable'; }); } -export function updateVeniceModelPricing(modelId) { - const el = document.getElementById('venice-model-pricing'); - if (el) el.innerHTML = renderModelPricingHint('venice', modelId || getVeniceModel()); -} export async function handleSaveVeniceKey() { const input = document.getElementById('venice-key-input'); @@ -612,55 +640,10 @@ export function handleRemoveVeniceKey() { window.openSettingsModal?.(); } -export function renderVeniceModelDropdown(models) { - const area = document.getElementById('venice-model-area'); - if (!area || !models.length) return; - const currentModel = getVeniceModel(); - const opts = buildModelOptions('venice', models, currentModel, function(m) { return m.name || m.id; }); - area.innerHTML = '' + - '' + - '
' + renderModelPricingHint('venice', currentModel) + '
'; -} - -export function onVeniceModelDropdownChange(value) { - const previous = getVeniceModel(); - setVeniceModel(value); - localStorage.setItem(getVeniceE2EE() ? 'labcharts-venice-model-e2ee' : 'labcharts-venice-model-regular', value); - if (previous !== value) window.clearE2EESession?.(); - updateVeniceModelPricing(value); -} - -export function toggleVeniceE2EE(on) { - setVeniceE2EE(on); - if (!on) window.clearE2EESession?.(); - // Swap model dropdown to E2EE or regular model list - const listKey = on ? 'labcharts-venice-e2ee-models' : 'labcharts-venice-models'; - let models = []; try { models = JSON.parse(localStorage.getItem(listKey) || '[]'); } catch {} - if (models.length) { - // Save current model for the mode we're leaving, restore the one for the mode we're entering - const prevKey = on ? 'labcharts-venice-model-regular' : 'labcharts-venice-model-e2ee'; - const restoreKey = on ? 'labcharts-venice-model-e2ee' : 'labcharts-venice-model-regular'; - localStorage.setItem(prevKey, getVeniceModel()); - const restored = localStorage.getItem(restoreKey); - const newModel = restored && models.some(m => m.id === restored) ? restored : models[0].id; - setVeniceModel(newModel); - renderVeniceModelDropdown(models); - } - const el = document.getElementById('venice-e2ee-indicator'); - if (el) el.style.display = on ? '' : 'none'; - window.updateChatHeaderModel?.(); - window.refreshWebSearchToggle?.(); -} - // ═══════════════════════════════════════════════ // OPENROUTER HANDLERS // ═══════════════════════════════════════════════ -export function updateOpenRouterModelPricing(modelId) { - const el = document.getElementById('openrouter-model-pricing'); - if (el) el.innerHTML = renderModelPricingHint('openrouter', modelId || getOpenRouterModel()); -} - export async function handleSaveOpenRouterKey() { const input = document.getElementById('openrouter-key-input'); const btn = document.getElementById('save-openrouter-key-btn'); @@ -698,19 +681,6 @@ export function handleRemoveOpenRouterKey() { window.openSettingsModal?.(); } -export function renderOpenRouterModelDropdown(models) { - const area = document.getElementById('openrouter-model-area'); - if (!area || !models.length) return; - const currentModel = getOpenRouterModel(); - const isCustom = !models.some(m => m.id === currentModel); - const opts = buildModelOptions('openrouter', models, currentModel, function(m) { return m.name || m.id; }); - area.innerHTML = '' + - '' + - '
' + - 'Press Enter to apply — checks model connectivity' + - '
' + renderModelPricingHint('openrouter', currentModel) + '
'; -} - function _orBalanceHtml(remaining) { const v = parseFloat(remaining); const color = v < 0.10 ? 'var(--red)' : v < 0.50 ? 'var(--yellow, #f0a800)' : 'var(--green)'; @@ -763,75 +733,10 @@ export function showInsufficientBalanceDialog() { overlay.onclick = function(e) { if (e.target === overlay) close(); }; } -export async function applyCustomOpenRouterModel(modelId) { - const id = modelId.trim(); - if (!id) return; - setOpenRouterModel(id); - // Show "checking..." while we verify the model - const pricingEl = document.getElementById('openrouter-model-pricing'); - if (pricingEl) pricingEl.innerHTML = 'Checking pricing\u2026'; - const select = document.getElementById('openrouter-model-select'); - const input = document.getElementById('openrouter-custom-model'); - const inDropdown = select && [...select.options].some(o => o.value === id); - if (select) { - if (inDropdown) { - select.value = id; - if (input) { input.value = ''; input.style.borderColor = ''; } - } else { - // Show "Custom model" placeholder in dropdown - let customOpt = select.querySelector('option[value="__custom"]'); - if (!customOpt) { - customOpt = document.createElement('option'); - customOpt.value = '__custom'; - customOpt.disabled = true; - customOpt.textContent = 'Using custom model'; - select.insertBefore(customOpt, select.firstChild); - } - customOpt.selected = true; - } - } - // Health check — verify model responds - const indicator = document.getElementById('openrouter-model-health'); - if (indicator) { indicator.textContent = '⏳'; indicator.title = 'Checking...'; indicator.style.color = 'var(--text-muted)'; } - try { - await window.callClaudeAPI({ messages: [{ role: 'user', content: 'hi' }], maxTokens: 1 }); - if (indicator) { indicator.textContent = '✓'; indicator.title = 'Model responding'; indicator.style.color = 'var(--green)'; } - if (input && !inDropdown) input.style.borderColor = 'var(--green)'; - showNotification('Model set: ' + id, 'info'); - // Fetch actual pricing and update display - await fetchOpenRouterModelPricing(id); - updateOpenRouterModelPricing(id); - } catch (e) { - if (indicator) { indicator.textContent = '✗'; indicator.title = e.message || 'Connection failed'; indicator.style.color = 'var(--red)'; } - if (input) input.style.borderColor = 'var(--red)'; - updateOpenRouterModelPricing(id); - showNotification('Model check failed: ' + (e.message || 'unknown error'), 'error'); - } -} - -export function onOpenRouterDropdownChange(value) { - setOpenRouterModel(value); - updateOpenRouterModelPricing(value); - const input = document.getElementById('openrouter-custom-model'); - if (input) { input.value = ''; input.style.borderColor = ''; } - const health = document.getElementById('openrouter-model-health'); - if (health) { health.textContent = ''; health.title = ''; } - // Remove "Using custom model" placeholder if present - const select = document.getElementById('openrouter-model-select'); - const customOpt = select?.querySelector('option[value="__custom"]'); - if (customOpt) customOpt.remove(); -} - - // ─── Routstr mode toggle ─── // Direct mode removed — wallet-only // ─── Routstr handlers ─── -export function updateRoutstrModelPricing(modelId) { - const el = document.getElementById('routstr-model-pricing'); - if (el) el.innerHTML = renderModelPricingHint('routstr', modelId || getRoutstrModel()); -} - export async function handleSaveRoutstrKey() { const input = document.getElementById('routstr-key-input'); const btn = document.getElementById('save-routstr-key-btn'); @@ -904,22 +809,6 @@ export function handleRemoveRoutstrKey() { window.openSettingsModal?.(); } -export function renderRoutstrModelDropdown(models) { - const area = document.getElementById('routstr-model-area'); - if (!area || !models.length) return; - let currentModel = getRoutstrModel(); - // Auto-select first model if stored model isn't available on this node - const modelIds = models.map(m => m.id); - if (currentModel && !modelIds.includes(currentModel)) { - currentModel = modelIds[0]; - setRoutstrModel(currentModel); - } - const opts = buildModelOptions('routstr', models, currentModel, function(m) { return m.name || m.id; }); - area.innerHTML = '' + - '' + - '
' + renderModelPricingHint('routstr', currentModel) + '
'; -} - // ─── PPQ handlers ─── let _ppqCreating = false; export async function handleCreatePpqAccount() { @@ -1021,21 +910,6 @@ export async function handleRemovePpqKey() { } } -export function renderPpqModelDropdown(models) { - const area = document.getElementById('ppq-model-area'); - if (!area || !models.length) return; - const currentModel = getPpqModel(); - const opts = buildModelOptions('ppq', models, currentModel, function(m) { return m.name || m.id; }); - area.innerHTML = '' + - '' + - '
' + renderModelPricingHint('ppq', currentModel) + '
'; -} - -export function updatePpqModelPricing(modelId) { - const el = document.getElementById('ppq-model-pricing'); - if (el) el.innerHTML = renderModelPricingHint('ppq', modelId); -} - function _ppqBalanceHtml(balance) { const v = parseFloat(balance); const color = v < 0.10 ? 'var(--red)' : v < 0.50 ? 'var(--yellow, #f0a800)' : 'var(--green)'; @@ -1274,37 +1148,6 @@ function handleRemoveCustomApi() { if (panel) panel.innerHTML = renderAIProviderPanel('custom'); } -function renderCustomApiModelDropdown(models) { - const area = document.getElementById('custom-model-area'); - if (!area) return; - const currentModel = getCustomApiModel(); - const opts = buildModelOptions('custom', models, currentModel, function(m) { return m.name || m.id; }); - const isCustom = !models.some(m => m.id === currentModel) && currentModel; - area.innerHTML = ` - -
-
${renderModelPricingHint('custom', currentModel)}
`; -} - -function updateCustomModelPricing(modelId) { - const el = document.getElementById('custom-model-pricing'); - if (el) el.innerHTML = renderModelPricingHint('custom', modelId || getCustomApiModel()); -} - -function applyCustomApiManualModel() { - const input = document.getElementById('custom-manual-model'); - if (!input) return; - const model = input.value.trim(); - if (!model) { showNotification('Enter a model ID', 'error'); return; } - setCustomApiModel(model); - // Update dropdown if it exists - const select = document.getElementById('custom-model-select'); - if (select) select.value = model; - updateCustomModelPricing(model); - showNotification('Model set to ' + model, 'success'); -} - - // ─── Model advisor helpers ─── function refreshModelAdvisor() { const details = window._lastOllamaModelDetails || []; diff --git a/service-worker.js b/service-worker.js index 860598d7..8bffb9be 100755 --- a/service-worker.js +++ b/service-worker.js @@ -188,6 +188,7 @@ const APP_SHELL = [ '/js/provider-qr.js', '/js/provider-wallet-panels.js', '/js/provider-panel-renderers.js', + '/js/provider-model-controls.js', '/js/provider-panels.js', '/js/dna.js', '/js/hardware.js', diff --git a/tests/test-audit.js b/tests/test-audit.js index 60616102..f2133b1c 100644 --- a/tests/test-audit.js +++ b/tests/test-audit.js @@ -58,6 +58,7 @@ const swAuditSrc = read('service-worker.js'); assert('SW uses importScripts for version', swAuditSrc.includes("importScripts('/version.js')")); assert('SW CACHE_NAME uses semver', swAuditSrc.includes('`labcharts-v${self.APP_VERSION}`')); assert('SW APP_SHELL includes API provider storage module', swAuditSrc.includes("'/js/api-provider-storage.js'")); +assert('SW APP_SHELL includes provider model controls module', swAuditSrc.includes("'/js/provider-model-controls.js'")); assert('SW APP_SHELL includes API transport module', swAuditSrc.includes("'/js/api-transport.js'")); assert('SW APP_SHELL includes PDF import review module', swAuditSrc.includes("'/js/pdf-import-review.js'")); assert('SW APP_SHELL includes PDF import support modules', diff --git a/tests/test-custom-api.js b/tests/test-custom-api.js index dbfb580c..cd07687d 100644 --- a/tests/test-custom-api.js +++ b/tests/test-custom-api.js @@ -217,7 +217,8 @@ console.log('\n12. settings.js + provider panel source inspection'); const settingsSrc = read('js/settings.js'); const panelsSrc = read('js/provider-panels.js'); const panelRenderSrc = read('js/provider-panel-renderers.js'); -const providerUiSrc = panelsSrc + panelRenderSrc; +const providerModelControlsSrc = read('js/provider-model-controls.js'); +const providerUiSrc = panelsSrc + panelRenderSrc + providerModelControlsSrc; assert('settings.js has data-provider="custom" button', settingsSrc.includes('data-provider="custom"')); assert("settings.js wires switchAIProvider('custom')", settingsSrc.includes("switchAIProvider('custom')")); assert('provider code imports getCustomApiUrl', providerUiSrc.includes('getCustomApiUrl')); @@ -225,14 +226,14 @@ assert('provider-panels imports setCustomApiUrl', panelsSrc.includes('setCustomA assert('provider code imports getCustomApiKey', providerUiSrc.includes('getCustomApiKey')); assert('provider-panels imports saveCustomApiKey', panelsSrc.includes('saveCustomApiKey')); assert('provider code imports getCustomApiModel', providerUiSrc.includes('getCustomApiModel')); -assert('provider-panels imports setCustomApiModel', panelsSrc.includes('setCustomApiModel')); +assert('provider-model-controls imports setCustomApiModel', providerModelControlsSrc.includes('setCustomApiModel')); assert('provider-panels imports fetchCustomApiModels', panelsSrc.includes('fetchCustomApiModels')); assert('provider-panels imports validateCustomApiKey', panelsSrc.includes('validateCustomApiKey')); assert('renderAIProviderPanel handles custom', panelRenderSrc.includes("provider === 'custom'")); assert('handleSaveCustomApi exists', panelsSrc.includes('function handleSaveCustomApi()')); assert('handleRemoveCustomApi exists', panelsSrc.includes('function handleRemoveCustomApi()')); -assert('renderCustomApiModelDropdown exists', panelsSrc.includes('function renderCustomApiModelDropdown(')); -assert('applyCustomApiManualModel exists', panelsSrc.includes('function applyCustomApiManualModel()')); +assert('renderCustomApiModelDropdown exists', providerModelControlsSrc.includes('function renderCustomApiModelDropdown(')); +assert('applyCustomApiManualModel exists', providerModelControlsSrc.includes('function applyCustomApiManualModel()')); assert('custom-url-input element', panelRenderSrc.includes('custom-url-input')); assert('custom-key-input element', panelRenderSrc.includes('custom-key-input')); assert('custom-model-area element', providerUiSrc.includes('custom-model-area')); diff --git a/tests/test-openrouter.js b/tests/test-openrouter.js index 49d1cfce..10ca54e4 100644 --- a/tests/test-openrouter.js +++ b/tests/test-openrouter.js @@ -98,11 +98,12 @@ else localStorage.removeItem('labcharts-openrouter-pricing'); console.log('\n3. provider panel source inspection'); const ppSrc = read('js/provider-panels.js'); const providerRenderSrc = read('js/provider-panel-renderers.js'); -const providerUiSrc = ppSrc + providerRenderSrc; +const providerModelControlsSrc = read('js/provider-model-controls.js'); +const providerUiSrc = ppSrc + providerRenderSrc + providerModelControlsSrc; assert('imports getOpenRouterKey', providerUiSrc.includes('getOpenRouterKey')); assert('imports saveOpenRouterKey', ppSrc.includes('saveOpenRouterKey')); assert('imports getOpenRouterModel', providerUiSrc.includes('getOpenRouterModel')); -assert('imports setOpenRouterModel', ppSrc.includes('setOpenRouterModel')); +assert('imports setOpenRouterModel', providerModelControlsSrc.includes('setOpenRouterModel')); assert('imports getOpenRouterModelDisplay', providerRenderSrc.includes('getOpenRouterModelDisplay')); assert('imports validateOpenRouterKey', ppSrc.includes('validateOpenRouterKey')); assert('imports fetchOpenRouterModels', ppSrc.includes('fetchOpenRouterModels')); @@ -114,8 +115,8 @@ assert('eager provider bridge persists selection synchronously', settingsSrc.inc assert('renderAIProviderPanel handles openrouter', providerRenderSrc.includes("provider === 'openrouter'")); assert('handleSaveOpenRouterKey exists', ppSrc.includes('function handleSaveOpenRouterKey()')); assert('handleRemoveOpenRouterKey exists', ppSrc.includes('function handleRemoveOpenRouterKey()')); -assert('renderOpenRouterModelDropdown exists', ppSrc.includes('function renderOpenRouterModelDropdown(')); -assert('updateOpenRouterModelPricing exists', ppSrc.includes('function updateOpenRouterModelPricing(')); +assert('renderOpenRouterModelDropdown exists', providerModelControlsSrc.includes('function renderOpenRouterModelDropdown(')); +assert('updateOpenRouterModelPricing exists', providerModelControlsSrc.includes('function updateOpenRouterModelPricing(')); assert('openrouter-key-input element', providerRenderSrc.includes('openrouter-key-input')); assert('openrouter-model-area element', providerUiSrc.includes('openrouter-model-area')); assert('openrouter-model-pricing element', providerUiSrc.includes('openrouter-model-pricing')); @@ -181,6 +182,7 @@ assert('SW uses importScripts for version', swSrc.includes("importScripts('/vers assert('SW CACHE_NAME uses semver', swSrc.includes('`labcharts-v${self.APP_VERSION}`')); assert('SW bypasses openrouter.ai', swSrc.includes('openrouter.ai')); assert('SW caches provider-panel-renderers.js', swSrc.includes('/js/provider-panel-renderers.js')); +assert('SW caches provider-model-controls.js', swSrc.includes('/js/provider-model-controls.js')); // ─── 7. Window function exports ─── console.log('\n7. Window function exports'); diff --git a/tests/test-venice-e2ee.js b/tests/test-venice-e2ee.js index 9705f897..38e96db6 100644 --- a/tests/test-venice-e2ee.js +++ b/tests/test-venice-e2ee.js @@ -361,11 +361,12 @@ try { // 16. Settings + Chat source checks const providerSrc = read('js/provider-panels.js'); const providerRenderSrc = read('js/provider-panel-renderers.js'); +const providerModelControlsSrc = read('js/provider-model-controls.js'); assert('provider renderer has venice-e2ee-toggle', providerRenderSrc.includes('venice-e2ee-toggle')); assert('provider renderer has venice-e2ee-indicator', providerRenderSrc.includes('venice-e2ee-indicator')); -assert('provider-panels has toggleVeniceE2EE', providerSrc.includes('toggleVeniceE2EE')); -assert('provider-panels has Venice model change handler', providerSrc.includes('function onVeniceModelDropdownChange')); -assert('Venice model dropdown uses change handler', (providerSrc + providerRenderSrc).includes('onchange="onVeniceModelDropdownChange(this.value)"')); +assert('provider model controls has toggleVeniceE2EE', providerModelControlsSrc.includes('function toggleVeniceE2EE')); +assert('provider model controls has Venice model change handler', providerModelControlsSrc.includes('function onVeniceModelDropdownChange')); +assert('Venice model dropdown uses change handler', (providerSrc + providerRenderSrc + providerModelControlsSrc).includes('onchange="onVeniceModelDropdownChange(this.value)"')); const chatSrc = read('js/chat.js'); const chatSendSrc = read('js/chat-send.js'); const chatAttestationSrc = read('js/chat-attestation.js'); diff --git a/version.js b/version.js index 56270ccd..da7a7af8 100644 --- a/version.js +++ b/version.js @@ -2,4 +2,4 @@ // Classic script (not ES module) so it works in both browser and service worker. // Browser: