0 directories, 46 files

util

Home / tinai / util
import Api from '../api.js';
import Users from '../users.js';
// The namespace import tolerates a stale cached dom-utils.js that predates
// update_range_fill: named imports of missing exports would crash the whole
// module graph until the PWA asset refresh loads the new file.
import * as dom_utils from './dom-utils.js';
import { getEl, setElementDisplay, addSafeEventListener } from './dom-utils.js';
import { escapeHtml } from './format-utils.js';
import { customConfirm } from './confirm-dialog.js';
import { showOverlay, hideOverlay } from './overlay-utils.js';
import { applyTheme, DEFAULT_THEME, fetchThemeIndex, getAllThemeKeys, getThemeFamilyForTheme, populateThemeFamilyDropdown, populateThemeDropdown } from './theme-utils.js';
import {
	formatThinkingLevelLabel,
	populateUtilityModelDropdown,
	calculateUtilityCost,
	calculateTtsCostPerMinute,
	formatEstimatedCost,
	populatePresetDropdown,
	populateFamilyDropdown,
	populateModelVersionDropdown,
	populateThinkingLevelDropdown,
	populateTtsModelDropdown,
	populateTtsVoiceDropdown,
	findMatchingPreset,
	formatModelFullName,
	updateModelDetailsDisplay,
	renderRankingCostTableHtml,
	updateVerbosityDescription
} from './settings-utils.js';

/** @typedef {import('../app.js').default} App */

/**
 * Preferred starting model per family, used when the user switches families.
 */
const DEFAULT_FAMILY_MODELS = {
	gemini: 'gemini-3.5-flash-lite'
};

/**
 * Returns the first thinking level that is not "off", falling back to the
 * first listed level when a model supports no active level.
 * @param {Array<string|null>} [thinkingModes] - Supported thinking mode values.
 * @returns {string|null}
 */
function firstActiveThinkingLevel(thinkingModes) {
	if (!Array.isArray(thinkingModes)) return null;
	const active = thinkingModes.find(m => m !== null && m !== undefined && m !== '' && !['none', 'off'].includes(String(m).toLowerCase()));
	return active !== undefined ? active : (thinkingModes[0] ?? null);
}

/**
 * Manages the app and conversation options overlays: applying settings to the UI,
 * persisting changes, and handling model/family/version/thinking level selection.
 */
class OptionsManager {

	/**
	 * @param {App} app - The main application instance.
	 */
	constructor(app) {
		this.app = app;
	}

	/**
	 * Updates the UI components when the global application configuration changes.
	 */
	on_app_config() {
		void this.apply_app_options();
		this.apply_app_defaults();
		this.apply_conversation_options();
		this.app.content_list?.apply_selected_index_class?.();
		this.app.content_list?.on_app_index_updated?.();
		this.app.on_conversation_updated();
		this.update_experimental_options_visibility();

		const config = this.app.storage.get_app_config();
		const experimental_features_enabled = config[this.app.storage.KEY_CONFIG_EXPERIMENTAL_FEATURES] || false;
		const documents_enabled = experimental_features_enabled && (config[this.app.storage.KEY_CONFIG_DOCUMENTS] || false);
		setElementDisplay(this.app.select_new_document, 'inline-block');
		for (const option of this.app.select_new_document?.options || []) {
			if (option.value && option.value !== 'PAGE') option.hidden = !documents_enabled;
		}
		const documentRow = getEl('id-div-empty-document-row');
		if (documentRow) {
			documentRow.style.display = 'flex';
		}
	}

	/**
	 * Saves the currently selected theme from the theme selector to the application configuration.
	 * @param {Event} e - The change event.
	 */
	update_app_options_setting(e) {
		let key = null;
		if (e.target.id === 'id-select-theme') {
			key = this.app.storage.KEY_CONFIG_THEME;
		} else if (e.target.id === 'id-select-utility-model') {
			key = this.app.storage.KEY_CONFIG_UTILITY_MODEL;
		} else if (e.target.id === 'id-select-tts-model') {
			key = this.app.storage.KEY_CONFIG_TTS_MODEL;
		} else if (e.target.id === 'id-select-tts-voice') {
			key = this.app.storage.KEY_CONFIG_TTS_VOICE;
		}
		if (key) {
			this.app.storage.update_app_config(key, e.target.value);
		}
	}

	async _on_theme_family_change(e) {
		const familyKey = e.target.value;
		const themeIndex = await fetchThemeIndex();
		const family = themeIndex?.[familyKey];
		if (!family || !Array.isArray(family.themes) || family.themes.length === 0) return;

		const config = this.app.storage.get_app_config();
		const currentTheme = config?.[this.app.storage.KEY_CONFIG_THEME];
		let newThemeKey = family.themes[0].key;
		if (family.themes.some(t => t.key === currentTheme)) {
			newThemeKey = currentTheme;
		}

		if (this.app.select_theme) {
			populateThemeDropdown(this.app.select_theme, themeIndex, familyKey, newThemeKey);
		}
		this.app.storage.update_app_config(this.app.storage.KEY_CONFIG_THEME, newThemeKey);
	}

	/**
	 * Updates the experimental features setting in the application configuration.
	 * @param {Event} e - The change event from the checkbox.
	 */
	update_experimental_features_setting(e) {
		this.app.storage.update_app_config(this.app.storage.KEY_CONFIG_EXPERIMENTAL_FEATURES, e.target.checked);
	}

	/**
	 * Loads the session tool configuration and renders the tool toggle list.
	 * @returns {Promise<void>}
	 */
	async load_tool_config() {
		const container = this.app.div_tool_config;
		if (!container) return;
		container.innerHTML = '<em>Loading tool configuration...</em>';
		const data = await this.app.api.get_tool_config();
		if (!data || data.error) {
			container.innerHTML = '<span class="span-status-error">Failed to load tool configuration.</span>';
			return;
		}
		const config = data.config || {};
		const tools = (data.tools || []).slice().sort((a, b) => (a.essential === b.essential) ? 0 : (a.essential ? 1 : -1));
		let html = '';
		for (const tool of tools) {
			const enabled = tool.essential || (config[tool.name] ?? tool.default_enabled);
			// Tooltip shows a short, user-friendly summary of the data or
			// functionality the tool provides, not the LLM-facing description.
			const tip = tool.user_description || tool.friendly_name;
			const title = tip ? ` title="${escapeHtml(tip)}"` : '';
			const id = `id-checkbox-tool-${escapeHtml(tool.name)}`;
			html += `<div class="div-field-row"${title}><input type="checkbox" id="${id}" data-tool="${escapeHtml(tool.name)}"${enabled ? ' checked' : ''}${tool.essential ? ' disabled' : ''}><label for="${id}">${escapeHtml(tool.friendly_name)}${tool.essential ? ' <em>(Essential)</em>' : ''}</label></div>`;
		}
		container.innerHTML = html;
		for (const checkbox of container.querySelectorAll('input[type="checkbox"][data-tool]')) {
			addSafeEventListener(checkbox, 'change', () => this.save_tool_config());
		}
	}

	/**
	 * Saves the current tool toggles to the session configuration.
	 * @returns {Promise<void>}
	 */
	async save_tool_config() {
		const container = this.app.div_tool_config;
		if (!container) return;
		const tools = {};
		for (const checkbox of container.querySelectorAll('input[type="checkbox"][data-tool]')) {
			tools[checkbox.dataset.tool] = checkbox.checked;
		}
		await this.app.api.save_tool_config(tools);
	}

	/**
	 * Loads the server-delivered experimental feature definitions and renders
	 * their checkboxes. Users without the experimental permission receive an
	 * empty list and never see the feature checkboxes at all.
	 * @returns {Promise<void>}
	 */
	async load_experimental_features() {
		const master = this.app.checkbox_experimental_features;
		const container = this.app.div_experimental_features_container;
		const list = this.app.div_experimental_features_list;
		if (!master || !container || !list) return;
		const data = await this.app.api.get_features();
		const allowed = !!(data && !data.error && data.experimental_allowed);
		const features = allowed && Array.isArray(data.features) ? data.features : [];
		setElementDisplay(container, allowed && features.length > 0 ? '' : 'none');

		const config = this.app.storage.get_app_config();
		if (!allowed) {
			// Revoke any stale client-side flag so dependent UI stays hidden
			if (config[this.app.storage.KEY_CONFIG_EXPERIMENTAL_FEATURES]) {
				this.app.storage.update_app_config(this.app.storage.KEY_CONFIG_EXPERIMENTAL_FEATURES, false);
			}
			list.innerHTML = '';
			this.update_experimental_options_visibility();
			return;
		}

		list.innerHTML = '';
		for (const feature of features) {
			// Resolve the storage constant (e.g. KEY_CONFIG_ATTACHMENTS -> 'ATTACHMENTS')
			// so the flag is saved under the same key the readers use.
			const configKey = this.app.storage['KEY_CONFIG_' + feature.key] || ('KEY_CONFIG_' + feature.key);
			const input = document.createElement('input');
			input.type = 'checkbox';
			input.id = 'id-checkbox-exp-' + feature.key;
			input.checked = config[configKey] || false;
			input.addEventListener('change', () => {
				this.app.storage.update_app_config(configKey, input.checked);
			});
			const label = document.createElement('label');
			label.htmlFor = input.id;
			label.textContent = feature.label;
			list.appendChild(input);
			list.appendChild(label);
			list.appendChild(document.createElement('br'));
		}
		this.update_experimental_options_visibility();
	}

	/**
	 * Whether the Collaboration experimental feature is enabled.
	 * @returns {boolean} True when experimental features and the Collaboration flag are both on.
	 */
	is_collaboration_enabled() {
		const config = this.app.storage.get_app_config();
		return !!(config[this.app.storage.KEY_CONFIG_EXPERIMENTAL_FEATURES] && config[this.app.storage.KEY_CONFIG_COLLABORATION]);
	}

	/**
	 * Shows or hides the experimental sub-options depending on the experimental
	 * features setting, and gates the Connect tab behind the Collaboration
	 * experimental feature.
	 */
	update_experimental_options_visibility() {
		if (!this.app.div_experimental_options) return;
		const config = this.app.storage.get_app_config();
		const experimental_features_enabled = config[this.app.storage.KEY_CONFIG_EXPERIMENTAL_FEATURES] || false;
		this.app.div_experimental_options.style.display = experimental_features_enabled ? 'block' : 'none';

		const collaboration_enabled = this.is_collaboration_enabled();
		const connect_button = getEl('id-btn-settings-tab-connect');
		setElementDisplay(connect_button, collaboration_enabled ? '' : 'none');
		if (!collaboration_enabled && getEl('id-div-settings-tab-connect')?.style.display !== 'none') {
			this.show_settings_tab('app');
		}
		this.app.chat_renderer?._update_sync_indicators();
	}

	/**
	 * Updates the maximum tool use setting in the application configuration.
	 * @param {Event} e - The change event from the number input.
	 */
	update_max_tool_use_setting(e) {
		const value = parseInt(e.target.value, 10);
		if (!isNaN(value) && value > 0) {
			this.app.storage.update_app_config(this.app.storage.KEY_CONFIG_MAX_TOOL_USE, value);
		}
	}

	/**
	 * Updates the "Allow Requests for Disabled Tool Use" setting in the application configuration.
	 * @param {Event} e - The change event from the checkbox.
	 */
	update_allow_tool_requests_setting(e) {
		this.app.storage.update_app_config(this.app.storage.KEY_CONFIG_ALLOW_TOOL_REQUESTS, e.target.checked);
	}

	/**
	 * Updates the UI zoom setting in the application configuration and applies it.
	 * @param {Event} e - The input event from the zoom slider.
	 */
	update_ui_zoom_setting(e) {
		this.set_ui_zoom(parseInt(e.target.value, 10));
		dom_utils.update_range_fill?.(e.target);
	}

	/**
	 * Persists the given UI zoom percentage and applies it.
	 * @param {number} value - Zoom percentage (70-200).
	 */
	set_ui_zoom(value) {
		this.app.storage.update_app_config(this.app.storage.KEY_CONFIG_UI_ZOOM, value);
		this.apply_ui_zoom();
	}

	/**
	 * Returns the configured UI zoom percentage, defaulting to 100 when unset or invalid.
	 * @returns {number} Zoom percentage (70-200).
	 */
	get_ui_zoom() {
		const value = parseInt(this.app.storage.get_app_config()[this.app.storage.KEY_CONFIG_UI_ZOOM], 10);
		return Number.isFinite(value) && value >= 70 && value <= 200 ? value : 100;
	}

	/**
	 * Applies the configured UI zoom level to the root element. Zoom on the
	 * root scales the initial containing block, so it reflows like native
	 * browser zoom instead of overflowing like transform scaling.
	 */
	apply_ui_zoom() {
		const zoom = this.get_ui_zoom();
		document.documentElement.style.zoom = zoom === 100 ? '' : String(zoom / 100);
		if (this.app.input_ui_zoom) {
			this.app.input_ui_zoom.value = String(zoom);
			dom_utils.update_range_fill?.(this.app.input_ui_zoom);
		}
		if (this.app.span_ui_zoom_value) {
			this.app.span_ui_zoom_value.textContent = zoom + '%';
		}
	}

	/**
	 * Applies the selected verbosity and model settings to the UI.
	 */
	apply_conversation_options() {
		this._apply_options('conversation');
	}

	/**
	 * Applies the default verbosity and model settings to the UI.
	 */
	apply_app_defaults() {
		this._apply_options('app');
	}

	/**
	 * Applies the selected CSS theme and updates Highlight.js stylesheet based on the configuration.
	 */
	async apply_app_options() {
		const config = this.app.storage.get_app_config();
		this.apply_ui_zoom();
		const themeIndex = await fetchThemeIndex();
		const allKeys = getAllThemeKeys(themeIndex);

		let theme = (config && config[this.app.storage.KEY_CONFIG_THEME]) ? config[this.app.storage.KEY_CONFIG_THEME] : DEFAULT_THEME;

		if (!allKeys.includes(theme)) {
			theme = DEFAULT_THEME;
		}

		const familyKey = getThemeFamilyForTheme(theme, themeIndex);

		if (this.app.select_theme_family) {
			populateThemeFamilyDropdown(this.app.select_theme_family, themeIndex, familyKey);
		}
		if (this.app.select_theme) {
			populateThemeDropdown(this.app.select_theme, themeIndex, familyKey, theme);
		}

		await applyTheme(theme, true, () => {
			this.app.initialize_mermaid();
			this.app.rerender_mermaid_diagrams();
		});

		const utility_model = (config && config[this.app.storage.KEY_CONFIG_UTILITY_MODEL]) ? config[this.app.storage.KEY_CONFIG_UTILITY_MODEL] : 'gemini-3.5-fl';
		if (this.app.select_utility_model) {
			populateUtilityModelDropdown(this.app.select_utility_model, this.app._visible_utility_models(), utility_model);
		}
		this.update_utility_model_cost_display();
		this.apply_tts_options(config);
	}

	/**
	 * Restores the Voice Model settings (model, voice) into their dropdowns,
	 * rebuilding the options from voice-models.json and falling back to the
	 * defaults for unknown saved values.
	 * @param {Object|null} config - The application configuration.
	 */
	apply_tts_options(config) {
		populateTtsModelDropdown(this.app.select_tts_model, Api.VOICE_MODELS, config?.[this.app.storage.KEY_CONFIG_TTS_MODEL]);
		populateTtsVoiceDropdown(this.app.select_tts_voice, Api.VOICE_MODELS, config?.[this.app.storage.KEY_CONFIG_TTS_VOICE]);
		const tts_model = config?.[this.app.storage.KEY_CONFIG_TTS_MODEL] || 'gemini-3.8-flash-lite-tts';
		const tts_voice = config?.[this.app.storage.KEY_CONFIG_TTS_VOICE] || 'charlie';
		const apply = (select, value, fallback) => {
			if (!select) return;
			select.value = [...select.options].some(option => option.value === value) ? value : fallback;
		};
		apply(this.app.select_tts_model, tts_model, 'gemini-3.8-flash-lite-tts');
		apply(this.app.select_tts_voice, tts_voice, 'charlie');
		this.update_tts_cost_display();
	}

	/**
	 * Updates the estimated per-minute readback cost display for the selected
	 * Voice Model settings.
	 */
	update_tts_cost_display() {
		if (!this.app.div_tts_cost) return;
		const model = this.app.select_tts_model?.value || 'gemini-3.8-flash-lite-tts';
		const costInfo = calculateTtsCostPerMinute(model);
		const formattedCost = formatEstimatedCost(costInfo.total);
		this.app.div_tts_cost.innerHTML = `<span class="span-utility-cost-label">Estimated Cost:</span><span class="span-utility-cost-val">${formattedCost}/min</span>`;
	}

	/**
	 * Updates the estimated cost display for the selected utility model based on two average sentences.
	 */
	update_utility_model_cost_display() {
		if (!this.app.div_utility_model_cost) return;
		const utilityKey = this.app.select_utility_model?.value || 'gemini-3.5-fl';
		const utilityConfig = Api.UTILITY_MODELS?.[utilityKey];
		const modelId = utilityConfig?.model || utilityKey;
		const thinking = utilityConfig?.thinking || null;
		const costInfo = calculateUtilityCost(modelId, thinking);
		const formattedCost = formatEstimatedCost(costInfo.total);
		this.app.div_utility_model_cost.innerHTML = `<span class="span-utility-cost-label">Estimated Cost:</span><span class="span-utility-cost-val">${formattedCost}</span>`;
	}

	/**
	 * Binds the settings overlay tab bar buttons.
	 */
	init_settings_tabs() {
		for (const name of this.settings_tab_names()) {
			addSafeEventListener(getEl(`id-btn-settings-tab-${name}`), 'click', () => this.show_settings_tab(name));
		}
		for (const name of this.conversation_tab_names()) {
			addSafeEventListener(getEl(`id-btn-conv-tab-${name}`), 'click', () => this.show_conversation_tab(name));
		}
	}

	/**
	 * Returns the settings tab names in tab bar order.
	 * @returns {Array<string>}
	 */
	settings_tab_names() {
		return ['me', 'app', 'style', 'default', 'connect', 'admin'];
	}

	/**
	 * Returns the conversation settings tab names in tab bar order.
	 * @returns {Array<string>}
	 */
	conversation_tab_names() {
		return ['model', 'options'];
	}

	/**
	 * Activates a tab in a tab bar, showing its panel and hiding the others.
	 * @param {string} prefix - Id prefix shared by the bar's buttons and panels.
	 * @param {Array<string>} names - Tab names in bar order.
	 * @param {string} name - The tab to activate.
	 */
	#show_tab(prefix, names, name) {
		for (const tab of names) {
			setElementDisplay(getEl(`id-div-${prefix}-tab-${tab}`), tab === name ? 'block' : 'none');
			getEl(`id-btn-${prefix}-tab-${tab}`)?.classList.toggle('active', tab === name);
		}
	}

	/**
	 * Activates the settings tab with the given name, showing its panel and
	 * hiding the others.
	 * @param {string} name - Tab name: me, app, style, default, connect or admin.
	 */
	show_settings_tab(name) {
		if (name === 'admin' && !Users.isAdmin) return;
		this.#show_tab('settings', this.settings_tab_names(), name);
	}

	/**
	 * Activates the conversation settings tab with the given name.
	 * @param {string} name - Tab name: model or options.
	 */
	show_conversation_tab(name) {
		this.#show_tab('conv', this.conversation_tab_names(), name);
	}

	/**
	 * Shows the application options form overlay.
	 */
	show_app_options() {
		showOverlay(this.app.div_app_options_overlay);
		requestAnimationFrame(() => {
			this.show_settings_tab('app');
			if (Users.instance) {
				Users.instance.loadProfile();
				if (Users.isAdmin) {
					Users.instance.loadAdminUsers();
				}
			}
			Users.update_admin_visibility();
			void this.apply_app_options();
			this.apply_app_defaults();
			void this.app.version_overlay.ensure_version_loaded();
			const config = this.app.storage.get_app_config();
			if (this.app.checkbox_experimental_features) {
				this.app.checkbox_experimental_features.checked = config[this.app.storage.KEY_CONFIG_EXPERIMENTAL_FEATURES] || false;
			}
			if (this.app.input_max_tool_use) {
				this.app.input_max_tool_use.value = config[this.app.storage.KEY_CONFIG_MAX_TOOL_USE] || 20;
			}
			if (this.app.checkbox_allow_tool_requests) {
				this.app.checkbox_allow_tool_requests.checked = config[this.app.storage.KEY_CONFIG_ALLOW_TOOL_REQUESTS] || false;
			}
			this.update_experimental_options_visibility();
			void this.load_experimental_features();
			void this.load_tool_config();
		});
	}

	/**
	 * Hides the application options form overlay.
	 */
	hide_app_options() {
		if (this.app.div_app_options_overlay) {
			hideOverlay(this.app.div_app_options_overlay);
		}
	}

	/**
	 * Shows the conversation options form overlay.
	 */
	show_conversation_options() {
		showOverlay(this.app.div_conversation_options_overlay);
		requestAnimationFrame(() => {
			this.show_conversation_tab('model');
			this.apply_conversation_options();
		});
	}

	/**
	 * Hides the conversation options form overlay.
	 */
	hide_conversation_options() {
		if (this.app.div_conversation_options_overlay) {
			hideOverlay(this.app.div_conversation_options_overlay);
		}
	}

	/**
	 * Populates the UI form fields with either default app options or conversation-specific settings.
	 * @param {('app'|'conversation')} type - The scope of settings to apply.
	 */
	_apply_options(type) {
		const is_app = type === 'app';
		const defaults = is_app ? this.app.storage.get_app_defaults() : null;
		const conversation = !is_app ? this.app.get_selected_conversation() : null;
		const app_defaults = !is_app ? this.app.storage.get_app_defaults() : null;

		const verbosity_key = is_app ? this.app.storage.KEY_CONFIG_DEFAULT_VERBOSITY : this.app.storage.KEY_CONVERSATION_VERBOSITY;
		const show_suggested_key = is_app ? this.app.storage.KEY_CONFIG_SHOW_SUGGESTED_QUERIES : this.app.storage.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES;
		const show_related_key = is_app ? this.app.storage.KEY_CONFIG_SHOW_RELATED_QUERIES : this.app.storage.KEY_CONVERSATION_SHOW_RELATED_QUERIES;
		const enforce_topics_key = is_app ? this.app.storage.KEY_CONFIG_ENFORCE_TOPICS : this.app.storage.KEY_CONVERSATION_ENFORCE_TOPICS;
		const auto_send_key = is_app ? this.app.storage.KEY_CONFIG_AUTO_RUN_PROPOSED_QUERIES : this.app.storage.KEY_CONVERSATION_AUTO_RUN_PROPOSED_QUERIES;

		const verbosity_val = is_app
			? (defaults ? defaults[verbosity_key] : 'standard')
			: (conversation ? (conversation[verbosity_key] || app_defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_VERBOSITY] || 'standard') : (app_defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_VERBOSITY] || 'standard'));

		const family_val = is_app
			? (defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY] || 'gemini')
			: (conversation?.[this.app.storage.KEY_CONVERSATION_MODEL_FAMILY] || app_defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY] || 'gemini');

		const version_val = is_app
			? (defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_MODEL_VERSION] || 'gemini-3.5-flash-lite')
			: (conversation?.[this.app.storage.KEY_CONVERSATION_MODEL_VERSION] || app_defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_MODEL_VERSION] || 'gemini-3.5-flash-lite');

		const thinking_val = is_app
			? (defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL] !== undefined ? defaults[this.app.storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL] : 'MINIMAL')
			: (conversation?.[this.app.storage.KEY_CONVERSATION_THINKING_LEVEL] !== undefined ? conversation[this.app.storage.KEY_CONVERSATION_THINKING_LEVEL] : (app_defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL] ?? 'MINIMAL'));

		const matchedPreset = findMatchingPreset(Api.PRESETS, family_val, version_val, thinking_val);
		const preset_val = is_app
			? (defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_MODEL] !== undefined ? defaults[this.app.storage.KEY_CONFIG_DEFAULT_MODEL] : matchedPreset)
			: (conversation?.[this.app.storage.KEY_CONVERSATION_MODEL] !== undefined ? conversation[this.app.storage.KEY_CONVERSATION_MODEL] : (conversation ? matchedPreset : (app_defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_MODEL] || matchedPreset)));

		const show_suggested_val = is_app
			? (defaults ? defaults[show_suggested_key] : false)
			: (conversation && conversation[show_suggested_key] !== undefined ? conversation[show_suggested_key] : (app_defaults ? app_defaults[this.app.storage.KEY_CONFIG_SHOW_SUGGESTED_QUERIES] : false));
		const show_related_val = is_app
			? (defaults ? defaults[show_related_key] : false)
			: (conversation && conversation[show_related_key] !== undefined ? conversation[show_related_key] : (app_defaults ? app_defaults[this.app.storage.KEY_CONFIG_SHOW_RELATED_QUERIES] : false));
		const enforce_topics_val = is_app
			? (defaults ? defaults[enforce_topics_key] : false)
			: (conversation && conversation[enforce_topics_key] !== undefined ? conversation[enforce_topics_key] : (app_defaults ? app_defaults[this.app.storage.KEY_CONFIG_ENFORCE_TOPICS] : false));
		const auto_send_val = is_app
			? (defaults ? defaults[auto_send_key] : false)
			: (conversation && conversation[auto_send_key] !== undefined ? conversation[auto_send_key] : (app_defaults ? app_defaults[this.app.storage.KEY_CONFIG_AUTO_RUN_PROPOSED_QUERIES] : false));
		const web_search_default_val = is_app
			? (defaults ? defaults[this.app.storage.KEY_CONFIG_ENABLE_WEB_SEARCH] : false)
			: false;
		const disable_safeguards_val = is_app
			? (defaults ? defaults[this.app.storage.KEY_CONFIG_DISABLE_SAFEGUARDS] : false)
			: (conversation && conversation[this.app.storage.KEY_CONVERSATION_DISABLE_SAFEGUARDS] !== undefined ? conversation[this.app.storage.KEY_CONVERSATION_DISABLE_SAFEGUARDS] : (app_defaults ? app_defaults[this.app.storage.KEY_CONFIG_DISABLE_SAFEGUARDS] : false));
		const encourage_creativity_val = is_app
			? (defaults ? defaults[this.app.storage.KEY_CONFIG_ENCOURAGE_CREATIVITY] : false)
			: (conversation && conversation[this.app.storage.KEY_CONVERSATION_ENCOURAGE_CREATIVITY] !== undefined ? conversation[this.app.storage.KEY_CONVERSATION_ENCOURAGE_CREATIVITY] : (app_defaults ? app_defaults[this.app.storage.KEY_CONFIG_ENCOURAGE_CREATIVITY] : false));

		const verbosity_select = is_app ? this.app.select_default_verbosity : this.app.select_conversation_verbosity;
		const preset_select = is_app ? this.app.select_default_model : this.app.select_conversation_model;
		const family_select = is_app ? this.app.select_default_model_family : this.app.select_conversation_model_family;
		const version_select = is_app ? this.app.select_default_model_version : this.app.select_conversation_model_version;
		const level_select = is_app ? this.app.select_default_thinking_level : this.app.select_conversation_model_level;

		const suggested_checkbox = is_app ? this.app.checkbox_default_show_suggestions : this.app.checkbox_conversation_show_suggestions;
		const related_checkbox = is_app ? this.app.checkbox_default_show_related : this.app.checkbox_conversation_show_related;
		const enforce_checkbox = is_app ? this.app.checkbox_default_enforce_topics : this.app.checkbox_conversation_enforce_topics;
		const auto_send_checkbox = is_app ? this.app.checkbox_default_auto_send_prompts : this.app.checkbox_conversation_auto_send_prompts;
		const play_chime_val = is_app ? (defaults ? defaults[this.app.storage.KEY_CONFIG_PLAY_CHIME] : false) : false;
		const synced_default_val = is_app ? !!defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_SYNCED] : false;

		if (verbosity_select) verbosity_select.value = verbosity_val || 'standard';

		const visible_families = this.app._visible_families();
		const selected_family = visible_families[family_val] ? family_val : 'gemini';

		populatePresetDropdown(preset_select, this.app._visible_presets(), preset_val);
		populateFamilyDropdown(family_select, visible_families, selected_family);

		const modelsList = visible_families[selected_family]?.models || [];
		populateModelVersionDropdown(version_select, modelsList, version_val);

		const modelObj = modelsList.find(m => m.model === version_val) || modelsList[0];
		const thinkingModes = modelObj?.thinking_modes || [];
		populateThinkingLevelDropdown(level_select, thinkingModes, thinking_val);

		const descEl = is_app ? this.app.div_default_model_description : this.app.div_conversation_model_description;
		const costEl = is_app ? this.app.div_default_model_costs : this.app.div_conversation_model_costs;
		updateModelDetailsDisplay(descEl, costEl, selected_family, version_val, thinking_val, Api.MODELS, verbosity_val);
		updateVerbosityDescription(is_app ? this.app.div_default_verbosity_description : this.app.div_conversation_verbosity_description, verbosity_val);

		if (!is_app) {
			this.#apply_conversation_mode_options(conversation, costEl, selected_family, version_val, thinking_val);
		}

		if (suggested_checkbox) suggested_checkbox.checked = show_suggested_val !== undefined ? show_suggested_val : false;
		if (related_checkbox) related_checkbox.checked = show_related_val !== undefined ? show_related_val : false;
		if (enforce_checkbox) enforce_checkbox.checked = !!enforce_topics_val;
		if (auto_send_checkbox) auto_send_checkbox.checked = !!auto_send_val;
		if (is_app && this.app.checkbox_default_play_chime) this.app.checkbox_default_play_chime.checked = !!play_chime_val;
		if (is_app && this.app.checkbox_default_synced) this.app.checkbox_default_synced.checked = synced_default_val;
		if (is_app && this.app.checkbox_default_web_search) this.app.checkbox_default_web_search.checked = !!web_search_default_val;
		if (is_app && this.app.checkbox_default_disable_safeguards) {
			this.app.checkbox_default_disable_safeguards.checked = !!disable_safeguards_val;
		}
		if (is_app && this.app.checkbox_default_encourage_creativity) {
			this.app.checkbox_default_encourage_creativity.checked = !!disable_safeguards_val && !!encourage_creativity_val;
			this.app.checkbox_default_encourage_creativity.disabled = !disable_safeguards_val;
		}

		if (!is_app) {
			if (this.app.checkbox_conversation_disable_safeguards) {
				this.app.checkbox_conversation_disable_safeguards.checked = !!disable_safeguards_val;
			}
			if (this.app.checkbox_conversation_encourage_creativity) {
				this.app.checkbox_conversation_encourage_creativity.checked = !!disable_safeguards_val && !!encourage_creativity_val;
				this.app.checkbox_conversation_encourage_creativity.disabled = !disable_safeguards_val;
			}

			const google_search_val = conversation ? conversation[this.app.storage.KEY_CONVERSATION_SEARCH] : false;
			if (this.app.checkbox_conversation_google_search) {
				this.app.checkbox_conversation_google_search.checked = !!google_search_val;
			}

			if (this.app.div_conversation_google_search_container) {
				this.app.div_conversation_google_search_container.style.display = 'block';
				const search_label = this.app.div_conversation_google_search_container.querySelector('label');
				if (search_label) {
					search_label.textContent = 'Enable Web Search';
				}
			}
		}
	}

	/**
	 * Adapts the conversation options form to the conversation type. Tool
	 * conversations (Ranking, Sticky Notes) hide the chat-only options that
	 * never show in their UI; Documents additionally hide the suggested,
	 * related, and interactive-prompt options. Ranking shows a cost table
	 * estimated by choice count instead of by verbosity.
	 * @param {Object|null} conversation - The selected conversation.
	 * @param {HTMLElement|null} costEl - The model cost table container.
	 * @param {string} selected_family - The selected model family.
	 * @param {string} version_val - The selected model version.
	 * @param {string|null} thinking_val - The selected thinking level.
	 */
	#apply_conversation_mode_options(conversation, costEl, selected_family, version_val, thinking_val) {
		const type = conversation?.[this.app.storage.KEY_CONVERSATION_TYPE];
		const isRanking = type === this.app.storage.CONVERSATION_TYPE_RANKING;
		const isSticky = type === this.app.storage.CONVERSATION_TYPE_STICKY_NOTES;
		const isTool = isRanking || isSticky;
		const isDocument = type === this.app.storage.CONVERSATION_TYPE_DOCUMENT;
		const toggle = (el, hidden) => {
			if (el) el.style.display = hidden ? 'none' : '';
		};
		toggle(this.app.div_conversation_verbosity_container, isTool);
		toggle(this.app.hr_conversation_model_divider, isTool);
		toggle(this.app.div_conversation_model_costs, isTool);
		toggle(this.app.div_conversation_suggested_container, isSticky || isDocument);
		toggle(this.app.div_conversation_enforce_topics_container, isTool);
		toggle(this.app.div_conversation_auto_send_container, isTool || isDocument);
		toggle(this.app.div_conversation_safeguards_container, isTool);

		if (isRanking && costEl) {
			costEl.innerHTML = renderRankingCostTableHtml(version_val, thinking_val);
			costEl.style.display = 'block';
		}
	}

	/**
	 * Handles change events on the model presets dropdown with confirmation prompt.
	 * @param {('app'|'conversation')} type - The scope of settings.
	 * @param {Event} e - The change event.
	 */
	async _on_preset_change(type, e) {
		const is_app = type === 'app';
		const newPresetKey = e.target.value;

		if (!newPresetKey) {
			if (is_app) {
				this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_MODEL, '');
			} else {
				const selected_guid = this.app.storage.get_app_config()[this.app.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
				if (selected_guid) {
					this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_MODEL, '');
				}
			}
			this._apply_options(type);
			this.app.on_conversation_updated();
			return;
		}

		const preset = Api.PRESETS?.[newPresetKey];
		if (!preset) return;

		const pFamily = preset.provider || 'gemini';
		const pModel = preset.model || 'gemini-3.5-flash-lite';
		const pThinking = preset.thinking !== undefined ? preset.thinking : null;

		const fullModelName = formatModelFullName(pFamily, pModel, null, Api.MODELS);
		const thinkingLabel = formatThinkingLevelLabel(pThinking);

		const confirmMsg = `Do you want to load preset "${preset.name || newPresetKey}"?\n\nModel: ${fullModelName}\nThinking Level: ${thinkingLabel}`;
		const confirmed = await customConfirm('Load Preset', confirmMsg, 'Load', 'Cancel');

		if (confirmed) {
			if (is_app) {
				this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_MODEL, newPresetKey);
				this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY, pFamily);
				this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_MODEL_VERSION, pModel);
				this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL, pThinking);
			} else {
				const selected_guid = this.app.storage.get_app_config()[this.app.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
				if (selected_guid) {
					this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_MODEL, newPresetKey);
					this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_MODEL_FAMILY, pFamily);
					this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_MODEL_VERSION, pModel);
					this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_THINKING_LEVEL, pThinking);
				}
			}
			this._apply_options(type);
			this.app.on_conversation_updated();
		} else {
			this._apply_options(type);
		}
	}

	/**
	 * Handles change events on the model family dropdown.
	 * @param {('app'|'conversation')} type - The scope of settings.
	 * @param {Event} e - The change event.
	 */
	_on_family_change(type, e) {
		const is_app = type === 'app';
		const newFamily = e.target.value;
		const modelsList = Api.MODELS?.[newFamily]?.models || [];
		const familyDefault = DEFAULT_FAMILY_MODELS[newFamily];
		const familyModel = modelsList.find(m => m.model === familyDefault) || modelsList[0];
		const firstModel = familyModel?.model || '';
		const firstThinking = firstActiveThinkingLevel(familyModel?.thinking_modes);

		const matchedPreset = findMatchingPreset(Api.PRESETS, newFamily, firstModel, firstThinking);

		if (is_app) {
			this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY, newFamily);
			this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_MODEL_VERSION, firstModel);
			this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL, firstThinking);
			this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_MODEL, matchedPreset);
		} else {
			const selected_guid = this.app.storage.get_app_config()[this.app.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
			if (selected_guid) {
				this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_MODEL_FAMILY, newFamily);
				this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_MODEL_VERSION, firstModel);
				this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_THINKING_LEVEL, firstThinking);
				this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_MODEL, matchedPreset);
			}
		}

		this._apply_options(type);
		this.app.on_conversation_updated();
	}

	/**
	 * Handles change events on the model version dropdown.
	 * @param {('app'|'conversation')} type - The scope of settings.
	 * @param {Event} e - The change event.
	 */
	_on_version_change(type, e) {
		const is_app = type === 'app';
		const newVersion = e.target.value;

		let family;
		let currentThinking;

		if (is_app) {
			const defaults = this.app.storage.get_app_defaults();
			family = defaults[this.app.storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY] || 'gemini';
			currentThinking = defaults[this.app.storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL];
		} else {
			const conversation = this.app.get_selected_conversation();
			const app_defaults = this.app.storage.get_app_defaults();
			family = conversation?.[this.app.storage.KEY_CONVERSATION_MODEL_FAMILY] || app_defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY] || 'gemini';
			currentThinking = conversation?.[this.app.storage.KEY_CONVERSATION_THINKING_LEVEL] !== undefined
				? conversation[this.app.storage.KEY_CONVERSATION_THINKING_LEVEL]
				: app_defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL];
		}

		const modelsList = Api.MODELS?.[family]?.models || [];
		const modelObj = modelsList.find(m => m.model === newVersion);
		const thinkingModes = modelObj?.thinking_modes || [];

		const normCurrentThinking = (currentThinking === null || currentThinking === undefined || currentThinking === '' || currentThinking === 'none') ? '' : String(currentThinking).toUpperCase();
		const isSupported = thinkingModes.some(mode => {
			const normMode = (mode === null || mode === undefined || mode === '' || mode === 'none') ? '' : String(mode).toUpperCase();
			return normMode === normCurrentThinking;
		});

		let nextThinking = currentThinking;
		if (!isSupported) {
			nextThinking = firstActiveThinkingLevel(thinkingModes);
		}

		const matchedPreset = findMatchingPreset(Api.PRESETS, family, newVersion, nextThinking);

		if (is_app) {
			this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_MODEL_VERSION, newVersion);
			this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL, nextThinking);
			this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_MODEL, matchedPreset);
		} else {
			const selected_guid = this.app.storage.get_app_config()[this.app.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
			if (selected_guid) {
				this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_MODEL_VERSION, newVersion);
				this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_THINKING_LEVEL, nextThinking);
				this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_MODEL, matchedPreset);
			}
		}

		this._apply_options(type);
		this.app.on_conversation_updated();
	}

	/**
	 * Handles change events on the thinking level dropdown.
	 * @param {('app'|'conversation')} type - The scope of settings.
	 * @param {Event} e - The change event.
	 */
	_on_thinking_level_change(type, e) {
		const is_app = type === 'app';
		const val = e.target.value;
		const newThinking = (val === '' || val === 'none') ? null : val;

		let family;
		let modelVersion;

		if (is_app) {
			const defaults = this.app.storage.get_app_defaults();
			family = defaults[this.app.storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY] || 'gemini';
			modelVersion = defaults[this.app.storage.KEY_CONFIG_DEFAULT_MODEL_VERSION] || 'gemini-3.5-flash-lite';
		} else {
			const conversation = this.app.get_selected_conversation();
			const app_defaults = this.app.storage.get_app_defaults();
			family = conversation?.[this.app.storage.KEY_CONVERSATION_MODEL_FAMILY] || app_defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY] || 'gemini';
			modelVersion = conversation?.[this.app.storage.KEY_CONVERSATION_MODEL_VERSION] || app_defaults?.[this.app.storage.KEY_CONFIG_DEFAULT_MODEL_VERSION] || 'gemini-3.5-flash-lite';
		}

		const matchedPreset = findMatchingPreset(Api.PRESETS, family, modelVersion, newThinking);

		if (is_app) {
			this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL, newThinking);
			this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_DEFAULT_MODEL, matchedPreset);
		} else {
			const selected_guid = this.app.storage.get_app_config()[this.app.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
			if (selected_guid) {
				this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_THINKING_LEVEL, newThinking);
				this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_MODEL, matchedPreset);
			}
		}

		this._apply_options(type);
		this.app.on_conversation_updated();
	}

	/**
	 * Updates a specific configuration setting from user interaction with a form element.
	 * @param {('app'|'conversation')} type - The scope of the setting.
	 * @param {Event} e - The input or change event.
	 */
	_update_setting(type, e) {
		const is_app = type === 'app';
		const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;

		const fieldMap = {
			'id-select-default-verbosity': this.app.storage.KEY_CONFIG_DEFAULT_VERBOSITY,
			'id-select-conversation-verbosity': this.app.storage.KEY_CONVERSATION_VERBOSITY,
			'id-checkbox-default-show-suggestions': this.app.storage.KEY_CONFIG_SHOW_SUGGESTED_QUERIES,
			'id-checkbox-default-show-related': this.app.storage.KEY_CONFIG_SHOW_RELATED_QUERIES,
			'id-checkbox-default-enforce-topics': this.app.storage.KEY_CONFIG_ENFORCE_TOPICS,
			'id-checkbox-default-auto-send-prompts': this.app.storage.KEY_CONFIG_AUTO_RUN_PROPOSED_QUERIES,
			'id-checkbox-default-play-chime': this.app.storage.KEY_CONFIG_PLAY_CHIME,
			'id-checkbox-default-synced': this.app.storage.KEY_CONFIG_DEFAULT_SYNCED,
			'id-checkbox-default-web-search': this.app.storage.KEY_CONFIG_ENABLE_WEB_SEARCH,
			'id-checkbox-default-disable-safeguards': this.app.storage.KEY_CONFIG_DISABLE_SAFEGUARDS,
			'id-checkbox-default-encourage-creativity': this.app.storage.KEY_CONFIG_ENCOURAGE_CREATIVITY,
			'id-checkbox-conversation-show-suggestions': this.app.storage.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES,
			'id-checkbox-conversation-show-related': this.app.storage.KEY_CONVERSATION_SHOW_RELATED_QUERIES,
			'id-checkbox-conversation-enforce-topics': this.app.storage.KEY_CONVERSATION_ENFORCE_TOPICS,
			'id-checkbox-conversation-auto-send-prompts': this.app.storage.KEY_CONVERSATION_AUTO_RUN_PROPOSED_QUERIES,
			'id-checkbox-conversation-disable-safeguards': this.app.storage.KEY_CONVERSATION_DISABLE_SAFEGUARDS,
			'id-checkbox-conversation-encourage-creativity': this.app.storage.KEY_CONVERSATION_ENCOURAGE_CREATIVITY,
			'id-checkbox-conversation-google-search': this.app.storage.KEY_CONVERSATION_SEARCH
		};

		const key = fieldMap[e.target.id];
		if (!key) return;

		if (is_app) {
			this.app.storage.update_app_defaults(key, value);
			if (key === this.app.storage.KEY_CONFIG_DISABLE_SAFEGUARDS && !value) {
				this.app.storage.update_app_defaults(this.app.storage.KEY_CONFIG_ENCOURAGE_CREATIVITY, false);
			}
		} else {
			const selected_guid = this.app.storage.get_app_config()[this.app.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
			if (selected_guid) {
				this.app.storage.update_conversation_field(selected_guid, key, value);
				if (key === this.app.storage.KEY_CONVERSATION_DISABLE_SAFEGUARDS && !value) {
					this.app.storage.update_conversation_field(selected_guid, this.app.storage.KEY_CONVERSATION_ENCOURAGE_CREATIVITY, false);
				}
			}
		}

		this._apply_options(type);
		this.app.on_conversation_updated();
	}

	/**
	 * Toggles a boolean configuration option for the currently active conversation.
	 * @param {string} key - The conversation option key to toggle.
	 */
	_toggle_conversation_option(key) {
		const selected_guid = this.app.storage.get_app_config()[this.app.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
		if (selected_guid) {
			const conversation = this.app.storage.get_conversation(selected_guid);
			const app_defaults = this.app.storage.get_app_defaults();
			const current_value = (conversation[key] !== undefined)
				? conversation[key]
				: (app_defaults[key] || false);
			this.app.storage.update_conversation_field(selected_guid, key, !current_value);
		}
	}

	/**
	 * Handles toggling the sync switch in the tab bar for the active conversation.
	 * Enabling asks for confirmation and flips the conversation's SYNCED
	 * setting; the local save then triggers the upload. Disabling removes the
	 * server copy after a confirmation.
	 * @param {Event} e - The change event.
	 */
	async _update_sync_setting(e) {
		const storage = this.app.storage;
		const selected_guid = storage.get_app_config()[storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
		if (!selected_guid || selected_guid === 'scratchpad') {
			e.target.checked = false;
			return;
		}

		const conversation = storage.get_conversation(selected_guid);
		const title = conversation?.[storage.KEY_CONVERSATION_TITLE] || 'Conversation';

		if (e.target.checked) {
			e.target.checked = false;
			const confirmed = await customConfirm(
				'Turn On Sync',
				`Turn on sync for "${title}"? Your data will be uploaded to our servers and synchronized between your devices.`,
				'Turn On',
				'Cancel',
				'Your data will be encrypted, but encryption is reversible. We will do everything possible to keep it safe, but consider keeping your most sensitive information local.'
			);
			if (!confirmed) {
				return;
			}
			e.target.checked = true;
			storage.update_conversation_field(selected_guid, storage.KEY_CONVERSATION_SYNCED, true);
			this.app.content_list?.on_app_index_updated?.();
			return;
		}

		e.target.checked = true;
		const confirmed = await customConfirm(
			'Turn Off Sync',
			`Are you sure you want to turn off synchronization for "${title}"? It will remain local to other devices where you have previously viewed it, but it will be removed from our servers.`,
			'Turn Off',
			'Cancel'
		);
		if (!confirmed) {
			return;
		}
		e.target.checked = false;
		storage.update_conversation_field(selected_guid, storage.KEY_CONVERSATION_SYNCED, false);
		this.app.content_list?.on_app_index_updated?.();
		await this.app.sync.disable_conversation(selected_guid);
	}
}

export default OptionsManager;
🌐
options-manager.js ×
Type: Web, text/plain
45.56 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File