mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-07-07 01:41:17 -03:00
feat(ui): redesign AI Provider settings with provider presets and model catalog
- Replace hardcoded provider list with PROVIDER_PRESETS (OpenAI, Ollama, DeepSeek, Groq, OpenRouter, OpenCode Go, Custom) - Load model lists from models.dev/api.json catalog at startup - Add Combobox vanilla JS component for model/base-URL selection - Fetch local Ollama models via live API instead of catalog - Hide API key values from frontend (boolean-only llm_api_key_set) - Add i18n translations for all 9+ locales - Update snapshot tests for new response fields
This commit is contained in:
@@ -1592,3 +1592,45 @@ input:checked + .toggle-slider:before {
|
||||
animation: settings-highlight-pulse 1.5s ease-in-out 3;
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
/* ---- Combobox panel for AI Provider settings ---- */
|
||||
/* The panel is appended to <body> by Combobox.js and positioned relative to
|
||||
the enhanced <input>. Styles reuse settings-modal CSS variables. */
|
||||
|
||||
.lm-combobox-panel {
|
||||
position: absolute;
|
||||
z-index: 10002;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
background: var(--lora-surface, #2a2a2a);
|
||||
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.12));
|
||||
border-radius: var(--border-radius-xs, 6px);
|
||||
box-shadow: var(--shadow-elevated, 0 6px 18px rgba(0, 0, 0, 0.45));
|
||||
font-size: 0.95em;
|
||||
color: var(--text-color, rgba(226, 232, 240, 0.9));
|
||||
padding: 4px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.lm-combobox-option {
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.lm-combobox-option:hover,
|
||||
.lm-combobox-option.is-active {
|
||||
background: rgba(from var(--lora-accent) r g b / 0.2);
|
||||
color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.lm-combobox-empty {
|
||||
padding: 8px 12px;
|
||||
color: var(--text-color);
|
||||
opacity: 0.45;
|
||||
font-style: italic;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
394
static/js/components/Combobox.js
Normal file
394
static/js/components/Combobox.js
Normal file
@@ -0,0 +1,394 @@
|
||||
// Combobox.js — Reusable dropdown-suggestion + free-text input component.
|
||||
//
|
||||
// Enhances an existing <input> element with a dropdown panel that merges static
|
||||
// `presets` with asynchronously fetched options (`fetchOptions`). The input
|
||||
// remains a free-text field — selecting a dropdown option is optional, the
|
||||
// user can always type an arbitrary value.
|
||||
//
|
||||
// Zero dependencies: pure DOM manipulation. Exported on `window.Combobox`
|
||||
// so non-module callers can instantiate it, and as a named ES module export
|
||||
// for callers that import it directly.
|
||||
//
|
||||
// Usage:
|
||||
// const box = new Combobox(inputEl, {
|
||||
// presets: ['masterpiece', 'best quality'],
|
||||
// fetchOptions: async (q) => await fetchSuggestions(q),
|
||||
// placeholder: 'Type a value…',
|
||||
// onSelect: (value) => console.log('chose', value),
|
||||
// });
|
||||
// box.updatePresets(['new', 'presets']);
|
||||
// box.setValue('masterpiece');
|
||||
|
||||
const DEBOUNCE_MS = 300;
|
||||
|
||||
export class Combobox {
|
||||
/**
|
||||
* @param {HTMLInputElement} inputElement Existing <input> to enhance.
|
||||
* @param {Object} options
|
||||
* @param {string[]} [options.presets=[]] Static preset values shown in dropdown.
|
||||
* @param {(inputValue: string) => Promise<string[]>} [options.fetchOptions]
|
||||
* Async function returning dynamic suggestions for the current input.
|
||||
* @param {string} [options.placeholder] Placeholder text for the empty state.
|
||||
* @param {(value: string) => void} [options.onSelect] Callback when an option is chosen.
|
||||
*/
|
||||
constructor(inputElement, options = {}) {
|
||||
if (!inputElement || inputElement.tagName !== 'INPUT') {
|
||||
console.error('Combobox: expected an <input> element');
|
||||
return;
|
||||
}
|
||||
|
||||
this.input = inputElement;
|
||||
this.presets = Array.isArray(options.presets) ? [...options.presets] : [];
|
||||
this.fetchOptions = typeof options.fetchOptions === 'function' ? options.fetchOptions : null;
|
||||
this.placeholder = options.placeholder || '';
|
||||
this.onSelect = typeof options.onSelect === 'function' ? options.onSelect : null;
|
||||
|
||||
// Internal state
|
||||
this._isOpen = false;
|
||||
this._activeIndex = -1;
|
||||
this._renderedOptions = []; // current visible option strings (de-duplicated, merged)
|
||||
this._fetchToken = 0; // guards against out-of-order async fetch results
|
||||
this._fetchTimer = null;
|
||||
this._suppressInputOpen = false; // guards setValue() from reopening the dropdown
|
||||
|
||||
this._buildDropdown();
|
||||
this._bindEvents();
|
||||
}
|
||||
|
||||
// ---- public API ----
|
||||
|
||||
/**
|
||||
* Replace the preset list. Re-renders the dropdown if it is open.
|
||||
* @param {string[]} presets
|
||||
* @returns {void}
|
||||
*/
|
||||
updatePresets(presets) {
|
||||
this.presets = Array.isArray(presets) ? [...presets] : [];
|
||||
if (this._isOpen) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the input value programmatically without triggering the dropdown
|
||||
* or firing synthetic events.
|
||||
* @param {string} value
|
||||
* @returns {void}
|
||||
*/
|
||||
setValue(value) {
|
||||
const prev = this._suppressInputOpen;
|
||||
this._suppressInputOpen = true;
|
||||
this.input.value = value ?? '';
|
||||
this._suppressInputOpen = prev;
|
||||
if (this._isOpen) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- build ----
|
||||
|
||||
_buildDropdown() {
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'lm-combobox-panel';
|
||||
panel.setAttribute('role', 'listbox');
|
||||
panel.style.display = 'none';
|
||||
// Append to <body> so the panel is never clipped by an overflow:hidden
|
||||
// ancestor; positioning is recomputed on each open.
|
||||
document.body.appendChild(panel);
|
||||
this.panel = panel;
|
||||
|
||||
if (this.placeholder) {
|
||||
this.input.setAttribute('placeholder', this.placeholder);
|
||||
}
|
||||
this.input.setAttribute('autocomplete', 'off');
|
||||
this.input.setAttribute('role', 'combobox');
|
||||
this.input.setAttribute('aria-autocomplete', 'list');
|
||||
this.input.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
// ---- event wiring ----
|
||||
|
||||
_bindEvents() {
|
||||
this.input.addEventListener('focus', () => {
|
||||
if (this._suppressInputOpen) return;
|
||||
this._open();
|
||||
});
|
||||
|
||||
this.input.addEventListener('input', () => {
|
||||
if (this._suppressInputOpen) return;
|
||||
this._open(); // no-op if already open
|
||||
this._refresh(); // re-filter by current input value
|
||||
this._scheduleFetch();
|
||||
});
|
||||
|
||||
this.input.addEventListener('keydown', (event) => this._onKeyDown(event));
|
||||
|
||||
// Click an option (delegated)
|
||||
this.panel.addEventListener('click', (event) => {
|
||||
const item = event.target.closest('.lm-combobox-option');
|
||||
if (!item) return;
|
||||
const value = item.dataset.value;
|
||||
if (value !== undefined) {
|
||||
this._choose(value);
|
||||
}
|
||||
});
|
||||
|
||||
// Hover updates the active highlight so keyboard + mouse stay in sync.
|
||||
this.panel.addEventListener('mouseover', (event) => {
|
||||
const item = event.target.closest('.lm-combobox-option');
|
||||
if (!item) return;
|
||||
const idx = Number(item.dataset.index);
|
||||
if (!Number.isNaN(idx)) {
|
||||
this._setActiveIndex(idx);
|
||||
}
|
||||
});
|
||||
|
||||
// Click outside closes the dropdown.
|
||||
this._outsideClickHandler = (event) => {
|
||||
if (this._isOpen && !this.input.contains(event.target) && !this.panel.contains(event.target)) {
|
||||
this._close();
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', this._outsideClickHandler);
|
||||
|
||||
// Reposition on viewport changes while open.
|
||||
this._resizeHandler = () => {
|
||||
if (this._isOpen) this._position();
|
||||
};
|
||||
window.addEventListener('resize', this._resizeHandler);
|
||||
window.addEventListener('scroll', this._resizeHandler, true);
|
||||
}
|
||||
|
||||
// ---- keyboard ----
|
||||
|
||||
_onKeyDown(event) {
|
||||
if (!this._isOpen) {
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
this._open();
|
||||
this._setActiveIndex(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
this._setActiveIndex(this._activeIndex + 1);
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
this._setActiveIndex(this._activeIndex - 1);
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
// Only intercept Enter to pick an option when one is actively
|
||||
// highlighted; otherwise let the input's default behavior
|
||||
// (form submit / free-text commit) proceed.
|
||||
if (this._activeIndex >= 0 && this._activeIndex < this._renderedOptions.length) {
|
||||
event.preventDefault();
|
||||
this._choose(this._renderedOptions[this._activeIndex]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
event.preventDefault();
|
||||
this._close();
|
||||
this.input.focus();
|
||||
break;
|
||||
|
||||
case 'Tab':
|
||||
// Allow normal tab navigation; just close the panel.
|
||||
this._close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- open / close ----
|
||||
|
||||
_open() {
|
||||
if (this._isOpen) return;
|
||||
this._isOpen = true;
|
||||
this.panel.style.display = 'block';
|
||||
this.input.setAttribute('aria-expanded', 'true');
|
||||
// On open, render ALL presets — do not filter by the current input
|
||||
// value. Filtering on the input event is handled separately.
|
||||
this._render(this.presets);
|
||||
this._position();
|
||||
}
|
||||
|
||||
_close() {
|
||||
if (!this._isOpen) return;
|
||||
this._isOpen = false;
|
||||
this.panel.style.display = 'none';
|
||||
this.input.setAttribute('aria-expanded', 'false');
|
||||
this._activeIndex = -1;
|
||||
this._cancelFetch();
|
||||
}
|
||||
|
||||
_position() {
|
||||
const rect = this.input.getBoundingClientRect();
|
||||
const panelHeight = this.panel.offsetHeight;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const spaceBelow = viewportHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
|
||||
// Flip above the input when there is more room there.
|
||||
const placeAbove = spaceBelow < panelHeight && spaceAbove > spaceBelow;
|
||||
const top = placeAbove
|
||||
? rect.top + window.scrollY - panelHeight
|
||||
: rect.bottom + window.scrollY;
|
||||
|
||||
this.panel.style.top = `${Math.max(0, top)}px`;
|
||||
this.panel.style.left = `${rect.left + window.scrollX}px`;
|
||||
this.panel.style.minWidth = `${rect.width}px`;
|
||||
}
|
||||
|
||||
// ---- rendering ----
|
||||
|
||||
/** Render a list of strings into the panel. */
|
||||
_render(items) {
|
||||
this._renderedOptions = items;
|
||||
this.panel.innerHTML = '';
|
||||
if (items.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'lm-combobox-empty';
|
||||
empty.textContent = this.placeholder ? this.placeholder : 'No options';
|
||||
this.panel.appendChild(empty);
|
||||
this._activeIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
items.forEach((opt, idx) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'lm-combobox-option';
|
||||
item.setAttribute('role', 'option');
|
||||
item.dataset.value = opt;
|
||||
item.dataset.index = String(idx);
|
||||
item.textContent = opt;
|
||||
if (idx === this._activeIndex) {
|
||||
item.classList.add('is-active');
|
||||
}
|
||||
fragment.appendChild(item);
|
||||
});
|
||||
this.panel.appendChild(fragment);
|
||||
|
||||
if (this._activeIndex >= items.length) {
|
||||
this._setActiveIndex(items.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Filter presets by current input value and re-render. */
|
||||
_refresh() {
|
||||
const value = this.input.value;
|
||||
const filtered = this._filterPresets(value);
|
||||
const merged = this._mergeUnique(filtered, this._fetchedOptions || []);
|
||||
this._render(merged);
|
||||
}
|
||||
|
||||
_filterPresets(value) {
|
||||
const v = (value || '').toLowerCase();
|
||||
if (!v) return [...this.presets];
|
||||
return this.presets.filter((p) => String(p).toLowerCase().startsWith(v));
|
||||
}
|
||||
|
||||
_mergeUnique(...lists) {
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
for (const list of lists) {
|
||||
for (const item of list) {
|
||||
const key = String(item);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
out.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
_setActiveIndex(idx) {
|
||||
const max = this._renderedOptions.length - 1;
|
||||
const clamped = Math.max(-1, Math.min(max, idx));
|
||||
this._activeIndex = clamped;
|
||||
// Update DOM classes without full re-render.
|
||||
const items = this.panel.querySelectorAll('.lm-combobox-option');
|
||||
items.forEach((el, i) => {
|
||||
el.classList.toggle('is-active', i === clamped);
|
||||
});
|
||||
// Scroll the active item into view inside the panel.
|
||||
if (clamped >= 0 && items[clamped]) {
|
||||
items[clamped].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the panel from the DOM and detach event listeners.
|
||||
* Call this before discarding the Combobox instance.
|
||||
*/
|
||||
destroy() {
|
||||
this._close();
|
||||
if (this.panel && this.panel.parentNode) {
|
||||
this.panel.parentNode.removeChild(this.panel);
|
||||
}
|
||||
document.removeEventListener('mousedown', this._outsideClickHandler);
|
||||
window.removeEventListener('resize', this._resizeHandler);
|
||||
window.removeEventListener('scroll', this._resizeHandler, true);
|
||||
}
|
||||
|
||||
_choose(value) {
|
||||
this.input.value = value;
|
||||
this._close();
|
||||
if (typeof this.onSelect === 'function') {
|
||||
this.onSelect(value);
|
||||
}
|
||||
// Re-focus without reopening the dropdown.
|
||||
this._suppressInputOpen = true;
|
||||
this.input.focus();
|
||||
this._suppressInputOpen = false;
|
||||
}
|
||||
|
||||
// ---- async fetch (debounced) ----
|
||||
|
||||
_scheduleFetch() {
|
||||
if (!this.fetchOptions) return;
|
||||
this._cancelFetch();
|
||||
this._fetchTimer = setTimeout(() => {
|
||||
this._fetchTimer = null;
|
||||
this._runFetch();
|
||||
}, DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
_cancelFetch() {
|
||||
if (this._fetchTimer) {
|
||||
clearTimeout(this._fetchTimer);
|
||||
this._fetchTimer = null;
|
||||
}
|
||||
this._fetchToken++; // invalidate any in-flight result
|
||||
}
|
||||
|
||||
async _runFetch() {
|
||||
if (!this.fetchOptions) return;
|
||||
const token = this._fetchToken;
|
||||
const value = this.input.value;
|
||||
let results;
|
||||
try {
|
||||
results = await this.fetchOptions(value);
|
||||
} catch (err) {
|
||||
console.error('Combobox fetchOptions error:', err);
|
||||
results = [];
|
||||
}
|
||||
// Stale guard: a newer fetch or close superseded this one.
|
||||
if (token !== this._fetchToken || !this._isOpen) return;
|
||||
this._fetchedOptions = Array.isArray(results) ? results : [];
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Expose for non-module callers (templates load via <script type="module">,
|
||||
// but some widget code reads globals off `window`).
|
||||
if (typeof window !== 'undefined') {
|
||||
window.Combobox = Combobox;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { initTheme, initBackToTop } from './utils/uiHelpers.js';
|
||||
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
||||
import { i18n } from './i18n/index.js';
|
||||
import { onboardingManager } from './managers/OnboardingManager.js';
|
||||
import './components/Combobox.js';
|
||||
import { BulkContextMenu } from './components/ContextMenu/BulkContextMenu.js';
|
||||
import { createPageContextMenu, createGlobalContextMenu } from './components/ContextMenu/index.js';
|
||||
import { initializeEventManagement } from './utils/eventManagementInit.js';
|
||||
|
||||
@@ -165,6 +165,18 @@ class AgentManager {
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
_readProviderRequiresKey(providerId) {
|
||||
const script = document.getElementById('llmProviderPresets');
|
||||
if (!script) return true; // safe default
|
||||
try {
|
||||
const presets = JSON.parse(script.textContent);
|
||||
const preset = presets[providerId];
|
||||
return preset ? preset.requires_key !== false : true;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async isLlmConfigured() {
|
||||
try {
|
||||
const response = await fetch('/api/lm/settings');
|
||||
@@ -172,8 +184,9 @@ class AgentManager {
|
||||
const data = await response.json();
|
||||
const provider = data.settings?.llm_provider;
|
||||
const hasModel = !!data.settings?.llm_model;
|
||||
const hasKey = !!data.settings?.llm_api_key;
|
||||
return hasModel && (hasKey || provider === 'ollama');
|
||||
const hasKey = !!(data.settings?.llm_api_key_set || data.settings?.llm_api_key);
|
||||
const needsKey = this._readProviderRequiresKey(provider);
|
||||
return hasModel && (hasKey || !needsKey);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -829,20 +829,106 @@ export class SettingsManager {
|
||||
this.updateApiKeyStatus();
|
||||
this.updateLlmApiKeyStatus();
|
||||
|
||||
// AI Provider settings
|
||||
// ── AI Provider settings ──────────────────────────────────────
|
||||
// Load provider presets from the JSON script tag embedded in the template
|
||||
this._providerPresets = {};
|
||||
this._providerModels = {};
|
||||
const presetsScript = document.getElementById('llmProviderPresets');
|
||||
if (presetsScript) {
|
||||
try {
|
||||
this._providerPresets = JSON.parse(presetsScript.textContent);
|
||||
} catch (_) {
|
||||
this._providerPresets = {};
|
||||
}
|
||||
}
|
||||
const modelsScript = document.getElementById('llmProviderModels');
|
||||
if (modelsScript) {
|
||||
try {
|
||||
this._providerModels = JSON.parse(modelsScript.textContent);
|
||||
} catch (_) {
|
||||
this._providerModels = {};
|
||||
}
|
||||
}
|
||||
|
||||
const llmProviderSelect = document.getElementById('llmProvider');
|
||||
if (llmProviderSelect) {
|
||||
llmProviderSelect.value = state.global.settings.llm_provider || 'openai';
|
||||
}
|
||||
|
||||
// Destroy previous combobox instances before creating new ones,
|
||||
// since loadSettingsToUI() runs on every modal open.
|
||||
if (this._llmApiBaseCombobox) { this._llmApiBaseCombobox.destroy(); }
|
||||
if (this._llmModelCombobox) { this._llmModelCombobox.destroy(); }
|
||||
|
||||
const llmApiBaseInput = document.getElementById('llmApiBase');
|
||||
if (llmApiBaseInput) {
|
||||
llmApiBaseInput.value = state.global.settings.llm_api_base || '';
|
||||
const presetUrls = Object.values(this._providerPresets)
|
||||
.map(p => p.api_base)
|
||||
.filter(Boolean);
|
||||
if (typeof Combobox !== 'undefined') {
|
||||
this._llmApiBaseCombobox = new Combobox(llmApiBaseInput, {
|
||||
presets: presetUrls,
|
||||
placeholder: 'https://api.openai.com/v1',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to update model Combobox presets from catalog / Ollama API
|
||||
const llmModelInput = document.getElementById('llmModel');
|
||||
if (llmModelInput) {
|
||||
llmModelInput.value = state.global.settings.llm_model || '';
|
||||
this._llmModelCombobox = null;
|
||||
if (llmModelInput && typeof Combobox !== 'undefined') {
|
||||
const currentProvider = llmProviderSelect ? llmProviderSelect.value : 'openai';
|
||||
const fallbackModels = currentProvider === 'ollama' ? [] : (this._providerModels[currentProvider] || []);
|
||||
this._llmModelCombobox = new Combobox(llmModelInput, {
|
||||
presets: fallbackModels,
|
||||
placeholder: translate('settings.aiProvider.modelPlaceholder', {}, 'Select a model...'),
|
||||
onSelect: (value) => {
|
||||
state.global.settings.llm_model = value;
|
||||
this.saveSetting('llm_model', value)
|
||||
.then(() => showToast('toast.settings.settingsUpdated', { setting: 'model' }, 'success'))
|
||||
.catch(() => {});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const _loadModelPresets = async (provider) => {
|
||||
if (!this._llmModelCombobox) return;
|
||||
if (provider === 'ollama') {
|
||||
try {
|
||||
const apiBase = document.getElementById('llmApiBase')?.value?.trim() || 'http://localhost:11434/v1';
|
||||
const resp = await fetch(`/api/lm/llm/models?provider=ollama&api_base=${encodeURIComponent(apiBase)}`);
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
if (data.success && Array.isArray(data.models)) {
|
||||
this._llmModelCombobox.updatePresets(data.models);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
this._llmModelCombobox.updatePresets([]);
|
||||
} else {
|
||||
this._llmModelCombobox.updatePresets(this._providerModels[provider] || []);
|
||||
}
|
||||
};
|
||||
_loadModelPresets(llmProviderSelect ? llmProviderSelect.value : 'openai');
|
||||
|
||||
// Provider change → auto-fill API Base URL + update model presets
|
||||
if (llmProviderSelect) {
|
||||
llmProviderSelect.addEventListener('change', () => {
|
||||
const provider = llmProviderSelect.value;
|
||||
const preset = this._providerPresets[provider];
|
||||
if (preset) {
|
||||
if (llmApiBaseInput && preset.api_base) {
|
||||
llmApiBaseInput.value = preset.api_base;
|
||||
if (this._llmApiBaseCombobox) {
|
||||
this._llmApiBaseCombobox.setValue(preset.api_base);
|
||||
}
|
||||
llmApiBaseInput.dispatchEvent(new Event('blur'));
|
||||
}
|
||||
}
|
||||
_loadModelPresets(provider);
|
||||
});
|
||||
}
|
||||
|
||||
const civitaiHostSelect = document.getElementById('civitaiHost');
|
||||
@@ -2949,7 +3035,7 @@ export class SettingsManager {
|
||||
}
|
||||
|
||||
updateLlmApiKeyStatus() {
|
||||
const hasKey = !!state.global.settings.llm_api_key;
|
||||
const hasKey = !!(state.global.settings.llm_api_key_set || state.global.settings.llm_api_key);
|
||||
const statusText = document.getElementById('llmApiKeyStatusText');
|
||||
const actionBtn = document.getElementById('llmApiKeyActionBtn');
|
||||
if (!statusText || !actionBtn) return;
|
||||
|
||||
Reference in New Issue
Block a user