-
🌐 async-utils.jsWeb, 3.69 kB⬇
-
🌐 auth-storage.jsWeb, 735 B⬇
-
🌐 chat-helpers.jsWeb, 8.85 kB⬇
-
🌐 chat-renderer.jsWeb, 70.00 kB⬇
-
🌐 code-overlay.jsWeb, 21.77 kB⬇
-
🌐 confirm-dialog.jsWeb, 3.17 kB⬇
-
🌐 conversation-format-utils.jsWeb, 21.51 kB⬇
-
🌐 diagram-overlay.jsWeb, 7.93 kB⬇
-
🌐 dialog.jsWeb, 2.50 kB⬇
-
🌐 dom-utils.jsWeb, 8.33 kB⬇
-
🌐 drag-reorder.jsWeb, 4.82 kB⬇
-
🌐 elements.jsWeb, 11.06 kB⬇
-
🌐 export-utils.jsWeb, 1.53 kB⬇
-
🌐 format-utils.jsWeb, 29.60 kB⬇
-
🌐 history-navigation.jsWeb, 1.95 kB⬇
-
🌐 hljs-yaml.jsWeb, 5.07 kB⬇
-
🌐 inline-format.jsWeb, 7.35 kB⬇
-
🌐 instant-answer.jsWeb, 19.82 kB⬇
-
🌐 interactive-tools.jsWeb, 38.77 kB⬇
-
🌐 mermaid-fixes.jsWeb, 4.60 kB⬇
-
🌐 mermaid-render.jsWeb, 5.10 kB⬇
-
🌐 mermaid.jsWeb, 195 B⬇
-
🌐 migrations.jsWeb, 12.11 kB⬇
-
🌐 multi-choice-dialog.jsWeb, 1.33 kB⬇
-
🌐 notification-stack.jsWeb, 2.50 kB⬇
-
🌐 options-manager.jsWeb, 45.56 kB⬇
-
🌐 overlay-utils.jsWeb, 4.14 kB⬇
-
🌐 pdf-export.jsWeb, 23.59 kB⬇
-
🌐 pinch-zoom.jsWeb, 3.21 kB⬇
-
🌐 plugin-dialog.jsWeb, 2.17 kB⬇
-
🌐 read-aloud.jsWeb, 33.84 kB⬇
-
🌐 response-pipeline.jsWeb, 55.19 kB⬇
-
🌐 scratchpad-editor.jsWeb, 8.64 kB⬇
-
🌐 scratchpad-export.jsWeb, 3.56 kB⬇
-
🌐 scrollbars.jsWeb, 3.12 kB⬇
-
🌐 selection-manager.jsWeb, 3.76 kB⬇
-
🌐 settings-utils.jsWeb, 31.08 kB⬇
-
🌐 share.jsWeb, 10.75 kB⬇
-
🌐 side-drawer.jsWeb, 9.37 kB⬇
-
🌐 speech-text.jsWeb, 6.60 kB⬇
-
🌐 swipe-navigation.jsWeb, 6.16 kB⬇
-
🌐 table-overlay.jsWeb, 8.10 kB⬇
-
🌐 theme-utils.jsWeb, 8.34 kB⬇
-
🌐 thinking-panel-base.jsWeb, 14.88 kB⬇
-
🌐 ui-sets.jsWeb, 1.00 kB⬇
-
🌐 version-overlay.jsWeb, 4.63 kB⬇
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
Type: Web, text/plain
45.56 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File