6 directories, 29 files

tinai

Home / tinai
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
import { escapeHtml, highlightCodeBlocks, formatThinkingLineHtml } from './util/format-utils.js';

import Api from './api.js';
import Users from './users.js';
import Conversation from './conversation.js';
import ConversationsList from './conversations.js';
import ConversationIndex from './conversation-index.js';
import Context from './context.js';
import Memory from './memory.js';
import NotebookIndex from './notebook-index.js';
import Document from './document.js';
import Diction from './diction.js';
import ScratchpadIndex from './scratchpad-index.js';
import ScratchpadConversation from './scratchpad-conversation.js';

import { getEl, queryEl, queryAll, setElementDisplay, toggleElementClass, addSafeEventListener } from './util/dom-utils.js';
import { debounce, copyToClipboard } from './util/async-utils.js';
import { customConfirm, customAlert } from './util/confirm-dialog.js';
import { showOverlay, hideOverlay } from './util/overlay-utils.js';
import { applyTheme, DEFAULT_THEME, fetchThemeIndex, getAllThemeKeys, getThemeFamilyForTheme, populateThemeFamilyDropdown, populateThemeDropdown } from './util/theme-utils.js';
import {
	formatThinkingLevelLabel,
	populateUtilityModelDropdown,
	calculateUtilityCost,
	getSearchUseCost,
	formatEstimatedCost,
	populatePresetDropdown,
	populateFamilyDropdown,
	populateModelVersionDropdown,
	populateThinkingLevelDropdown,
	findMatchingPreset,
	formatModelFullName,
	formatModelShortName,
	updateModelDetailsDisplay,
	formatVersionHeader,
	formatChangelogItem
} from './util/settings-utils.js';
import { formatCostSummary, formatAnnotationsBlock } from './util/conversation-format-utils.js';

/**
 * Main application class for TinAI.
 * Manages UI state, layout, local storage synchronization, and coordinates
 * communication between the API service and conversation rendering.
 */
class App {

	SCROLL_DELAY = 30;
	BREAKPOINT_MOBILE = 780;
	BREAKPOINT_TABLET = 1560;

	#api = new Api();
	#audioCtx = null;
	#wakeLock = null;
	#scrollTimeout = null;
	#conversation = new Conversation();
	#context;
	#memory = new Memory();
	#notebook_index;
	#document = new Document();
	#diction = new Diction();
	#scratchpad_index;
	#scratchpad_conversation = new ScratchpadConversation();
	#storage;
	#conversations_list;
	#conversation_index;
	#prompt_drafts = {};
	#active_conversation_guid = null;
	#app_version_data = null;

	btn_empty_new_chat;
	btn_empty_new_notebook;
	btn_empty_new_scratchpad;
	btn_close;
	btn_costs;
	btn_app_options;
	btn_conversation_options;
	btn_options_close;
	btn_instant_answer;
	div_instant_answer_overlay;
	btn_instant_answer_close;
	btn_instant_answer_send;
	btn_instant_answer_search;
	select_instant_answer_model;
	div_instant_answer_model_cost;
	instant_answer_query;
	div_instant_answer_status;
	div_instant_answer_response_container;
	div_instant_answer_response;
	div_instant_answer_cost;
	btn_show_changelog;
	btn_version_close;
	div_version_overlay;
	div_version_content;
	div_app_version_title;
	div_app_version_description;
	btn_send;
	prompt;
	div_title;
	div_subtitle;
	div_list;
	div_index_list;
	div_response;
	select_theme;
	select_utility_model;
	div_utility_model_cost;
	select_default_verbosity;
	select_conversation_verbosity;
	select_default_model;
	select_default_model_family;
	select_default_model_version;
	select_default_thinking_level;
	select_conversation_model;
	select_conversation_model_family;
	select_conversation_model_version;
	select_conversation_model_level;
	checkbox_experimental_features;
	checkbox_background_keep_alive;
	div_experimental_options;
	checkbox_experimental_mistral;
	checkbox_experimental_organizer;
	checkbox_default_show_suggestions;
	checkbox_default_show_related;
	checkbox_default_enforce_topics;
	checkbox_default_auto_send_prompts;
	checkbox_conversation_show_suggestions;
	checkbox_conversation_show_related;
	checkbox_conversation_enforce_topics;
	checkbox_conversation_auto_send_prompts;
	checkbox_conversation_google_search;
	div_conversation_google_search_container;
	btn_conversation_new;
	btn_conversation_list_new;
	btn_select_conversations;
	btn_archive_conversations;
	btn_delete_conversations;
	btn_archives_toggle;
	btn_show_conversations_new;
	btn_show_scratchpads_new;
	btn_conversations;
	btn_conversation_index;
	btn_conversation_index_select;
	btn_conversation_index_favorite;
	btn_conversation_index_delete;
	div_chat_empty;
	div_chat_empty_header;
	div_chat_ui_elements;
	div_options_chips;
	span_option_model;
	span_option_verbosity;
	span_option_suggested;
	span_option_related;
	span_option_search;
	span_option_enforce_topics;
	span_option_auto_run;
	div_dialog_backdrop;
	div_app_options_overlay;
	div_conversation_options_overlay;
	div_options_overlay;
	btn_app_options_close;
	btn_conversation_options_close;
	form_app_options;
	form_profile_options;
	form_conversation_options;
	form_account_options;
	form_admin_options;
	form_add_funds;
	span_subtitle_toggle;
	btn_tab_conversation;
	btn_tab_context;
	btn_tab_memory;
	btn_tab_document;
	btn_tab_diction;
	btn_tab_notebook_memory;
	div_chat_container_scroll;
	div_chat_context_scroll;
	div_chat_memory_scroll;
	div_notebook_memory_scroll;
	div_chat_document_scroll;
	div_chat_diction_scroll;
	div_chat_prompt_inset;
	div_chat_prompt_container;
	div_chat_title_bar;
	div_chat_tab_bar;
	div_notebook_tab_bar;
	div_structure_center_notebook_options;
	div_structure_center_index_options;

	state_v_open;
	state_i_open;
	state_active_tab = 'conversation';

	//region Initialization

	/**
	 * Initializes the application state, mermaid diagrams, UI elements, and event listeners.
	 * @param {Storage} storage - Storage manager instance.
	 */
	constructor(storage) {
		this.#storage = storage;
		const initial_config = this.#storage.get_app_config();
		const initial_guid = initial_config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
		const isScratchpadRoot = (initial_guid === 'scratchpad');
		const initialConv = initial_guid && !isScratchpadRoot ? this.#storage.get_conversation(initial_guid) : null;
		const isScratchpadConv = initialConv?.[this.#storage.KEY_CONVERSATION_TYPE] === this.#storage.CONVERSATION_TYPE_SCRATCHPAD;
		const isScratchpad = isScratchpadRoot || isScratchpadConv;

		this.state_v_open = window.innerWidth > this.BREAKPOINT_MOBILE;
		this.state_i_open = isScratchpad;
		if (isScratchpad && window.innerWidth <= this.BREAKPOINT_TABLET) {
			this.state_v_open = false;
		}

		if (typeof mermaid !== 'undefined') {
			try {
				mermaid.initialize({
					startOnLoad: false,
					securityLevel: 'loose',
					theme: 'base',
					themeVariables: {
						darkMode: true,
						background: 'transparent',
						primaryColor: 'var(--BG-panel2)',
						primaryTextColor: 'var(--FG-base)',
						primaryBorderColor: 'var(--AC-trim)',
						lineColor: 'var(--AC-trim)',
						secondaryColor: 'var(--BG-panel)',
						tertiaryColor: 'var(--BG-base)',
						edgeLabelBackground: 'var(--BG-panel2)',
						clusterBkg: 'var(--BG-base)',
						clusterBorder: 'var(--BD-panel-l)',
						defaultLinkColor: 'var(--AC-trim)',
						titleColor: 'var(--FG-base)',
						nodeBorder: 'var(--AC-trim)',
						nodeTextColor: 'var(--FG-base)',
						actorBkg: 'var(--BG-panel2)',
						actorBorder: 'var(--AC-trim)',
						actorTextColor: 'var(--FG-base)',
						actorLineColor: 'var(--BD-panel-l)',
						signalColor: 'var(--AC-trim)',
						signalTextColor: 'var(--FG-base)',
						labelBoxBkgColor: 'var(--BG-panel2)',
						labelBoxBorderColor: 'var(--BD-panel-l)',
						labelTextColor: 'var(--FG-base)',
						loopTextColor: 'var(--FG-base)',
						noteBkgColor: 'var(--BG-base)',
						noteBorderColor: 'var(--BD-panel-l)',
						noteTextColor: 'var(--FG-base)'
					},
					suppressErrorRendering: true,
					useMaxWidth: true
				});
			} catch (e) {
				console.error('Failed to initialize mermaid:', e);
			}
		}

		this.init_elements();
		this._populate_model_dropdowns();
		this.init_conversations_list();
		this.init_conversation_index();
		this.init_notebook_index();
		this.init_scratchpad_index();
		this.init_context();
		this.init_listeners();
		this.init_interactions();
		this.on_app_config();
		this.#conversations_list?.on_app_index_updated?.();
		this.handle_responsive_layout(window.innerWidth, window.innerWidth);
		this.apply_panels_layout();
		this.validate_prompt();

		this.scroll_to_last_item();
		void this.init_version_check();
	}

	/**
	 * Checks the latest app version against stored version and shows changelog overlay if newer.
	 * Also populates version information in application settings.
	 * @returns {Promise<void>}
	 */
	async init_version_check() {
		try {
			const versionData = await this.#api.get_version();
			if (!versionData || versionData.error) return;

			this.#app_version_data = versionData;
			this.update_app_version_settings(versionData);

			const latestDate = versionData['version-date'];
			if (!latestDate) return;

			const storedDate = this.#storage.get_stored_version_date();

			if (!storedDate || String(latestDate) > String(storedDate)) {
				await this.show_version_overlay();
				this.#storage.set_stored_version_date(latestDate);
			}
		} catch (error) {
			console.error('Failed to check app version:', error);
		}
	}

	/**
	 * Fetches the latest app version and updates the application options display.
	 * @returns {Promise<void>}
	 */
	async load_app_version() {
		if (this.#app_version_data) {
			this.update_app_version_settings(this.#app_version_data);
			return;
		}
		try {
			const versionData = await this.#api.get_version();
			if (versionData && !versionData.error) {
				this.#app_version_data = versionData;
				this.update_app_version_settings(versionData);
			}
		} catch (error) {
			console.error('Failed to load version for settings:', error);
		}
	}

	/**
	 * Populates the version section in the application settings form.
	 * @param {Object} versionData - Latest version object.
	 */
	update_app_version_settings(versionData) {
		if (!versionData) return;
		if (this.div_app_version_title) {
			this.div_app_version_title.textContent = formatVersionHeader(versionData);
		}
		if (this.div_app_version_description) {
			this.div_app_version_description.textContent = versionData['version-description'] || '';
		}
	}

	/**
	 * Shows the full version overlay with current and previous version changelogs.
	 * @returns {Promise<void>}
	 */
	async show_version_overlay() {
		if (!this.div_version_overlay) return;

		try {
			const response = await this.#api.get_version(2);
			const versions = response?.versions || (response ? [response] : []);
			this.render_version_overlay_content(versions);
			showOverlay(this.div_version_overlay);
		} catch (error) {
			console.error('Failed to load full version information:', error);
		}
	}

	/**
	 * Hides the full version overlay modal.
	 */
	hide_version_overlay() {
		if (this.div_version_overlay) {
			hideOverlay(this.div_version_overlay);
		}
	}

	/**
	 * Renders current and previous version details into the version overlay container.
	 * @param {Array<Object>} versions - Array of version data objects.
	 */
	render_version_overlay_content(versions) {
		if (!this.div_version_content) return;
		if (!Array.isArray(versions) || versions.length === 0) {
			this.div_version_content.innerHTML = '<p>No version information available.</p>';
			return;
		}

		const current = versions[0];
		const previous = versions.length > 1 ? versions[1] : null;

		let html = '';

		if (current) {
			html += `<fieldset class="div-version-section">
				<legend>Current Version</legend>
				<p style="font-weight: bold; margin-bottom: 4px;">${formatVersionHeader(current)}</p>
				<p style="margin-bottom: 8px;">${current['version-description'] || ''}</p>`;
			if (Array.isArray(current.changelog) && current.changelog.length > 0) {
				html += `<ul>`;
				current.changelog.forEach(item => {
					html += `<li>${formatChangelogItem(item)}</li>`;
				});
				html += `</ul>`;
			}
			html += `</fieldset>`;
		}

		if (previous) {
			html += `<fieldset class="div-version-section" style="margin-top: 15px;">
				<legend>Previous Version</legend>
				<p style="font-weight: bold; margin-bottom: 4px;">${formatVersionHeader(previous)}</p>
				<p style="margin-bottom: 8px;">${previous['version-description'] || ''}</p>`;
			if (Array.isArray(previous.changelog) && previous.changelog.length > 0) {
				html += `<ul>`;
				previous.changelog.forEach(item => {
					html += `<li>${formatChangelogItem(item)}</li>`;
				});
				html += `</ul>`;
			}
			html += `</fieldset>`;
		}

		this.div_version_content.innerHTML = html;
	}

	/**
	 * Maps DOM elements to class properties for structured access.
	 */
	init_elements() {
		this.btn_close = getEl('btn-close');
		this.btn_costs = getEl('btn-costs');
		this.btn_app_options = getEl('btn-app-options');
		this.btn_conversation_options = getEl('id-btn-conversation-options');
		this.btn_options_close = getEl('btn-options-close');
		this.btn_send = getEl('btn-send');
		this.prompt = getEl('id-prompt');
		this.div_title = getEl('id-div-response-title');
		this.div_subtitle = getEl('id-div-response-subtitle');
		this.div_response = getEl('id-div-response-render');
		this.select_theme_family = getEl('id-select-theme-family');
		this.select_theme = getEl('id-select-theme');
		this.select_utility_model = getEl('id-select-utility-model');
		this.div_utility_model_cost = getEl('id-div-utility-model-cost');
		this.select_default_verbosity = getEl('id-select-default-verbosity');
		this.select_conversation_verbosity = getEl('id-select-conversation-verbosity');
		this.select_default_model = getEl('id-select-default-model');
		this.select_default_model_family = getEl('id-select-default-model-family');
		this.select_default_model_version = getEl('id-select-default-model-version');
		this.select_default_thinking_level = getEl('id-select-default-thinking-level');
		this.select_conversation_model = getEl('id-select-conversation-model');
		this.select_conversation_model_family = getEl('id-select-conversation-model-family');
		this.select_conversation_model_version = getEl('id-select-conversation-model-version');
		this.select_conversation_model_level = getEl('id-select-conversation-model-level');
		this.div_default_model_description = getEl('id-div-default-model-description');
		this.div_default_model_costs = getEl('id-div-default-model-costs');
		this.div_conversation_model_description = getEl('id-div-conversation-model-description');
		this.div_conversation_model_costs = getEl('id-div-conversation-model-costs');
		this.checkbox_experimental_features = getEl('id-checkbox-experimental-features');
		this.checkbox_background_keep_alive = getEl('id-checkbox-background-keep-alive');
		this.div_experimental_options = getEl('id-div-experimental-options');
		this.checkbox_experimental_mistral = getEl('id-checkbox-experimental-mistral');
		this.checkbox_experimental_organizer = getEl('id-checkbox-experimental-organizer');
		this.checkbox_default_show_suggestions = getEl('id-checkbox-default-show-suggestions');
		this.checkbox_default_show_related = getEl('id-checkbox-default-show-related');
		this.checkbox_default_enforce_topics = getEl('id-checkbox-default-enforce-topics');
		this.checkbox_default_auto_send_prompts = getEl('id-checkbox-default-auto-send-prompts');
		this.checkbox_default_play_chime = getEl('id-checkbox-default-play-chime');
		this.checkbox_default_web_search = getEl('id-checkbox-default-web-search');
		this.div_default_web_search_container = getEl('id-div-default-web-search-container');
		this.checkbox_conversation_show_suggestions = getEl('id-checkbox-conversation-show-suggestions');
		this.checkbox_conversation_show_related = getEl('id-checkbox-conversation-show-related');
		this.checkbox_conversation_enforce_topics = getEl('id-checkbox-conversation-enforce-topics');
		this.checkbox_conversation_auto_send_prompts = getEl('id-checkbox-conversation-auto-send-prompts');
		this.checkbox_conversation_google_search = getEl('id-checkbox-conversation-google-search');
		this.div_conversation_google_search_container = getEl('id-div-conversation-google-search-container');
		this.div_list = getEl('id-div-list');
		this.div_index_list = getEl('id-div-center-index-list');
		this.btn_conversation_new = getEl('id-btn-conversation-new');
		this.btn_conversation_list_new = getEl('id-btn-conversation-list-new');
		this.btn_select_conversations = getEl('id-btn-select-conversations');
		this.btn_archive_conversations = getEl('id-btn-archive-conversation');
		this.btn_archives_toggle = getEl('id-btn-conversation-archives');
		this.btn_delete_conversations = getEl('id-btn-delete-conversation');
		this.btn_empty_new_chat = getEl('id-btn-empty-new-chat');
		this.btn_empty_new_notebook = getEl('id-btn-empty-new-notebook');
		this.btn_empty_new_scratchpad = getEl('id-btn-empty-new-scratchpad');
		this.btn_show_conversations_new = getEl('id-btn-show-conversations-new');
		this.btn_show_scratchpads_new = getEl('id-btn-show-scratchpads-new');
		this.btn_conversations = getEl('id-btn-conversations');
		this.btn_conversation_index = getEl('id-btn-conversation-index');
		this.btn_conversation_index_select = getEl('id-btn-conversation-index-select');
		this.btn_conversation_index_favorite = getEl('id-btn-conversation-index-favorite');
		this.btn_conversation_index_delete = getEl('id-btn-conversation-index-delete');
		this.div_chat_empty = queryEl('.div-chat-empty');
		this.div_chat_empty_header = queryEl('.div-chat-empty-header');
		this.div_chat_ui_elements = queryAll('.div-chat-ui-elements');
		this.div_options_chips = queryEl('.div-options-chips');
		this.span_option_model = getEl('id-span-option-model');
		this.span_option_verbosity = getEl('id-span-option-verbosity');
		this.span_option_suggested = getEl('id-span-option-suggested');
		this.span_option_related = getEl('id-span-option-related');
		this.span_option_search = getEl('id-span-option-search');
		this.span_option_enforce_topics = getEl('id-span-option-enforce-topics');
		this.span_option_auto_run = getEl('id-span-option-auto-run');
		this.div_dialog_backdrop = getEl('id-dialog-backdrop');
		this.btn_instant_answer = getEl('id-btn-instant-answer');
		this.div_instant_answer_overlay = getEl('id-instant-answer-overlay');
		this.btn_instant_answer_close = getEl('btn-instant-answer-close');
		this.btn_instant_answer_send = getEl('btn-instant-answer-send');
		this.btn_instant_answer_search = getEl('btn-instant-answer-search');
		this.select_instant_answer_model = getEl('id-select-instant-answer-model');
		this.div_instant_answer_model_cost = getEl('id-div-instant-answer-model-cost');
		this.instant_answer_query = getEl('id-instant-answer-query');
		this.div_instant_answer_status = getEl('id-instant-answer-status');
		this.div_instant_answer_response_container = getEl('id-instant-answer-response-container');
		this.div_instant_answer_response = getEl('id-instant-answer-response');
		this.div_instant_answer_cost = getEl('id-instant-answer-cost');
		this.div_app_options_overlay = getEl('id-app-options-overlay');
		this.div_conversation_options_overlay = getEl('id-conversation-options-overlay');
		this.div_options_overlay = this.div_app_options_overlay;
		this.btn_app_options_close = getEl('btn-options-close');
		this.btn_options_close = this.btn_app_options_close;
		this.btn_conversation_options_close = getEl('btn-conversation-options-close');
		this.div_version_overlay = getEl('id-version-dialog-overlay');
		this.btn_version_close = getEl('btn-version-close');
		this.btn_show_changelog = getEl('btn-show-changelog');
		this.div_app_version_title = getEl('id-div-app-version-title');
		this.div_app_version_description = getEl('id-div-app-version-description');
		this.div_version_content = getEl('id-div-version-content');
		this.form_app_options = getEl('id-form-app-options');
		this.form_profile_options = getEl('id-form-profile-options');
		this.form_conversation_options = getEl('id-form-conversation-options');
		this.form_account_options = getEl('id-form-account-options');
		this.form_admin_options = getEl('id-form-admin-options');
		this.form_add_funds = getEl('id-form-add-funds');
		this.span_subtitle_toggle = getEl('id-span-subtitle-toggle');
		this.btn_tab_conversation = getEl('id-btn-tab-conversation');
		this.btn_tab_context = getEl('id-btn-tab-context');
		this.btn_tab_memory = getEl('id-btn-tab-memory');
		this.btn_tab_document = getEl('id-btn-tab-document');
		this.btn_tab_diction = getEl('id-btn-tab-diction');
		this.btn_tab_notebook_memory = getEl('id-btn-tab-notebook-memory');
		this.div_chat_container_scroll = getEl('id-chat-container-scroll');
		this.div_chat_context_scroll = getEl('div-chat-context-scroll');
		this.div_chat_memory_scroll = getEl('div-chat-memory-scroll');
		this.div_notebook_memory_scroll = getEl('div-notebook-memory-scroll');
		this.div_chat_document_scroll = getEl('div-chat-document-scroll');
		this.div_chat_diction_scroll = getEl('div-chat-diction-scroll');
		this.div_chat_prompt_inset = getEl('div-chat-prompt-inset');
		this.div_chat_prompt_container = getEl('div-chat-prompt-container');
		this.div_chat_title_bar = queryEl('.div-chat-title-bar');
		this.div_chat_tab_bar = queryEl('.div-chat-tab-bar');
		this.div_notebook_tab_bar = queryEl('.div-notebook-tab-bar');
		this.div_structure_center_notebook_options = getEl('div-structure-center-notebook-options');
		this.div_structure_center_index_options = queryEl('.div-structure-center-index-options');
		this._reset_chat_view();
	}

	/**
	 * Populates the model dropdowns with options from Api.MODELS and Api.PRESETS.
	 * @private
	 */
	_populate_model_dropdowns() {
		populatePresetDropdown(this.select_default_model, this._visible_presets());
		populatePresetDropdown(this.select_conversation_model, this._visible_presets());
		populateFamilyDropdown(this.select_default_model_family, this._visible_families());
		populateFamilyDropdown(this.select_conversation_model_family, this._visible_families());
	}

	/**
	 * Returns model families visible in the UI, hiding experimental providers unless enabled.
	 * @returns {Object} Filtered families data from models.json.
	 */
	_visible_families() {
		const config = this.#storage.get_app_config();
		const mistral_enabled = (config[this.#storage.KEY_CONFIG_EXPERIMENTAL_FEATURES] && config[this.#storage.KEY_CONFIG_ENABLE_MISTRAL]) || false;
		const families = {};
		for (const key in Api.MODELS) {
			if (!Api.MODELS[key]?.experimental || mistral_enabled) {
				families[key] = Api.MODELS[key];
			}
		}
		return families;
	}

	/**
	 * Returns presets visible in the UI, filtering out those belonging to hidden experimental families.
	 * @returns {Object} Filtered presets from model-presets.json.
	 */
	_visible_presets() {
		const families = this._visible_families();
		const presets = {};
		for (const key in Api.PRESETS) {
			const provider = Api.PRESETS[key]?.provider;
			if (!Api.MODELS?.[provider] || families[provider]) {
				presets[key] = Api.PRESETS[key];
			}
		}
		return presets;
	}

	/**
	 * Returns utility models visible in the UI, filtering out those belonging to hidden experimental families.
	 * @returns {Object} Filtered utility models from utility-models.json.
	 */
	_visible_utility_models() {
		const families = this._visible_families();
		const utilityModels = {};
		for (const key in Api.UTILITY_MODELS) {
			const provider = Api.UTILITY_MODELS[key]?.provider;
			if (!Api.MODELS?.[provider] || families[provider]) {
				utilityModels[key] = Api.UTILITY_MODELS[key];
			}
		}
		return utilityModels;
	}

	/**
	 * Initializes the ConversationsList instance.
	 */
	init_conversations_list() {
		const app_callbacks = {
			update_app_config: this.#storage.update_app_config.bind(this.#storage),
			apply_panels_layout: this.apply_panels_layout.bind(this),
			render_conversation_header: this.render_conversation_header.bind(this),
			close_conversation: this.close_conversation.bind(this),
			set_v_open: (value) => { this.state_v_open = value; },
			set_i_open: (value) => { this.state_i_open = value; },
			on_conversation_updated: this.on_conversation_updated.bind(this),
			set_active_tab: this.set_active_tab.bind(this),
			is_request_active: (guid) => this.#api.is_request_active(guid),
			is_scratchpad_active_loading: () => this._is_any_scratchpad_loading(),
			abort_request: (guid) => this.stop_response(guid)
		};

		const elements = {
			div_list: this.div_list,
			div_title: this.div_title,
			btn_select_conversations: this.btn_select_conversations,
			btn_archive_conversations: this.btn_archive_conversations,
			btn_delete_conversations: this.btn_delete_conversations,
			btn_archives_toggle: this.btn_archives_toggle,
			btn_conversations: this.btn_conversations,
			btn_show_conversations_new: this.btn_show_conversations_new
		};

		const breakpoints = {
			BREAKPOINT_MOBILE: this.BREAKPOINT_MOBILE,
			BREAKPOINT_TABLET: this.BREAKPOINT_TABLET
		};

		this.#conversations_list = new ConversationsList(this.#storage, app_callbacks, elements, breakpoints);
	}

	/**
	 * Initializes the ConversationIndex instance.
	 */
	init_conversation_index() {
		const app_callbacks = {
			get_selected_conversation: this.get_selected_conversation.bind(this),
			set_i_open: (value) => { this.state_i_open = value; },
			apply_panels_layout: this.apply_panels_layout.bind(this),
			on_conversation_updated_main_panel: this.on_conversation_updated.bind(this)
		};

		const elements = {
			div_index_list: this.div_index_list,
			btn_conversation_index_select: this.btn_conversation_index_select,
			btn_conversation_index_favorite: this.btn_conversation_index_favorite,
			btn_conversation_index_delete: this.btn_conversation_index_delete
		};

		const breakpoints = {
			BREAKPOINT_MOBILE: this.BREAKPOINT_MOBILE
		};

		this.#conversation_index = new ConversationIndex(this.#storage, app_callbacks, elements, breakpoints, this.SCROLL_DELAY);
	}

	/**
	 * Initializes the NotebookIndex instance.
	 */
	init_notebook_index() {
		this.#notebook_index = new NotebookIndex(this.#storage, {});
	}

	/**
	 * Initializes the ScratchpadIndex instance with callbacks and DOM elements.
	 */
	init_scratchpad_index() {
		const app_callbacks = {
			get_selected_conversation: this.get_selected_conversation.bind(this),
			set_i_open: (value) => { this.state_i_open = value; },
			apply_panels_layout: this.apply_panels_layout.bind(this),
			on_conversation_updated_main_panel: this.on_conversation_updated.bind(this),
			close_conversation: this.close_conversation.bind(this),
			update_app_config: (key, val) => this.#storage.update_app_config(key, val),
			create_new_scratchpad: () => this.#conversations_list?.index_new?.(this.#storage.CONVERSATION_TYPE_SCRATCHPAD),
			is_request_active: (guid) => this.#api.is_request_active(guid),
			abort_request: (guid) => this.stop_response(guid)
		};

		const elements = {
			div_index_list: this.div_index_list,
			btn_conversation_index_select: this.btn_conversation_index_select,
			btn_conversation_index_favorite: this.btn_conversation_index_favorite,
			btn_conversation_index_delete: this.btn_conversation_index_delete
		};

		const breakpoints = {
			BREAKPOINT_MOBILE: this.BREAKPOINT_MOBILE
		};

		this.#scratchpad_index = new ScratchpadIndex(this.#storage, app_callbacks, elements, breakpoints);
	}

	/**
	 * Initializes the Context instance.
	 */
	init_context() {
		const app_callbacks = {
			renderContext: this.render_context_tab.bind(this)
		};
		this.#context = new Context(this.#storage, app_callbacks);
	}

	/**
	 * Sets up event listeners for window storage changes, window resizing, and scroll events.
	 */
	init_listeners() {
		window.addEventListener('storage', (event) => {
			this.handle_storage_change(event);
		});

		window.addEventListener('tinai:request-start', (event) => {
			const guid = event.detail?.guid;
			this.update_conversation_loading_indicators(guid, true);
		});

		window.addEventListener('tinai:request-stop', (event) => {
			const guid = event.detail?.guid;
			this.update_conversation_loading_indicators(guid, false);
			const selected_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID] || '';
			if (!guid || guid === selected_guid) {
				if (!this.#api.is_request_active(selected_guid)) {
					this.hide_progress_ui();
				}
			}
		});

		window.addEventListener('tinai:thinking', (event) => {
			const text = event.detail?.text;
			const guid = event.detail?.guid;
			if (text !== undefined && text !== null) {
				this.send_thinking(text, guid);
			}
		});

		let lastWidth = window.innerWidth;
		const debouncedResize = debounce(() => {
			const currentWidth = window.innerWidth;
			this.handle_responsive_layout(lastWidth, currentWidth);
			this.resize_prompt_textarea();
			this.resize_instant_answer_textarea();
			lastWidth = currentWidth;
			this.apply_panels_layout();
		}, 100);

		window.addEventListener('resize', debouncedResize);

		const scrollContainer = queryEl('.div-chat-container-scroll');
		if (scrollContainer) {
			scrollContainer.addEventListener('scroll', () => {
				if (this.#conversation_index) {
					this.#conversation_index.highlight_active_index_item();
				}
			});
		}
	}

	/**
	 * Binds user interaction events such as clicks and keyboard inputs to their respective handlers.
	 */
	init_interactions() {
		addSafeEventListener(this.btn_send, 'click', this.send.bind(this));

		if (this.prompt) {
			this.prompt.oninput = () => {
				const config = this.#storage.get_app_config();
				const current_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID] || '';
				this.#prompt_drafts[current_guid] = this.prompt.value;
				this.validate_prompt();
				this.resize_prompt_textarea();
			};
			this.prompt.onkeydown = this.handle_keydown.bind(this);
		}

		addSafeEventListener(this.select_theme_family, 'change', (e) => this._on_theme_family_change(e));
		addSafeEventListener(this.select_theme, 'change', (e) => this.update_app_options_setting(e));
		addSafeEventListener(this.select_utility_model, 'change', (e) => {
			this.update_app_options_setting(e);
			this.update_utility_model_cost_display();
		});
		addSafeEventListener(this.select_default_verbosity, 'change', (e) => this._update_setting('app', e));
		addSafeEventListener(this.select_conversation_verbosity, 'change', (e) => this._update_setting('conversation', e));
		addSafeEventListener(this.select_default_model, 'change', (e) => this._on_preset_change('app', e));
		addSafeEventListener(this.select_conversation_model, 'change', (e) => this._on_preset_change('conversation', e));
		addSafeEventListener(this.select_default_model_family, 'change', (e) => this._on_family_change('app', e));
		addSafeEventListener(this.select_conversation_model_family, 'change', (e) => this._on_family_change('conversation', e));
		addSafeEventListener(this.select_default_model_version, 'change', (e) => this._on_version_change('app', e));
		addSafeEventListener(this.select_conversation_model_version, 'change', (e) => this._on_version_change('conversation', e));
		addSafeEventListener(this.select_default_thinking_level, 'change', (e) => this._on_thinking_level_change('app', e));
		addSafeEventListener(this.select_conversation_model_level, 'change', (e) => this._on_thinking_level_change('conversation', e));

		addSafeEventListener(this.checkbox_experimental_features, 'change', (e) => this.update_experimental_features_setting(e));
		addSafeEventListener(this.checkbox_background_keep_alive, 'change', (e) => this.update_background_keep_alive_setting(e));
		addSafeEventListener(this.checkbox_experimental_mistral, 'change', (e) => this.update_experimental_mistral_setting(e));
		addSafeEventListener(this.checkbox_experimental_organizer, 'change', (e) => this.update_experimental_organizer_setting(e));
		addSafeEventListener(this.checkbox_default_show_suggestions, 'change', (e) => this._update_setting('app', e));
		addSafeEventListener(this.checkbox_default_show_related, 'change', (e) => this._update_setting('app', e));
		addSafeEventListener(this.checkbox_default_enforce_topics, 'change', (e) => this._update_setting('app', e));
		addSafeEventListener(this.checkbox_default_auto_send_prompts, 'change', (e) => this._update_setting('app', e));
		addSafeEventListener(this.checkbox_default_play_chime, 'change', (e) => this._update_setting('app', e));
		addSafeEventListener(this.checkbox_default_web_search, 'change', (e) => this._update_setting('app', e));

		addSafeEventListener(this.checkbox_conversation_show_suggestions, 'change', (e) => this._update_setting('conversation', e));
		addSafeEventListener(this.checkbox_conversation_show_related, 'change', (e) => this._update_setting('conversation', e));
		addSafeEventListener(this.checkbox_conversation_show_related, 'change', (e) => this._update_setting('conversation', e));
		addSafeEventListener(this.checkbox_conversation_enforce_topics, 'change', (e) => this._update_setting('conversation', e));
		addSafeEventListener(this.checkbox_conversation_auto_send_prompts, 'change', (e) => this._update_setting('conversation', e));
		addSafeEventListener(this.checkbox_conversation_google_search, 'change', (e) => this._update_setting('conversation', e));

		if (this.div_default_model_costs) {
			addSafeEventListener(this.div_default_model_costs, 'click', (e) => {
				const tr = e.target.closest('tr[data-verbosity]');
				if (tr && this.select_default_verbosity) {
					const verbosity = tr.getAttribute('data-verbosity');
					if (verbosity && this.select_default_verbosity.value !== verbosity) {
						this.select_default_verbosity.value = verbosity;
						this.select_default_verbosity.dispatchEvent(new Event('change'));
					}
				}
			});
		}

		if (this.div_conversation_model_costs) {
			addSafeEventListener(this.div_conversation_model_costs, 'click', (e) => {
				const tr = e.target.closest('tr[data-verbosity]');
				if (tr && this.select_conversation_verbosity) {
					const verbosity = tr.getAttribute('data-verbosity');
					if (verbosity && this.select_conversation_verbosity.value !== verbosity) {
						this.select_conversation_verbosity.value = verbosity;
						this.select_conversation_verbosity.dispatchEvent(new Event('change'));
					}
				}
			});
		}

		addSafeEventListener(this.btn_conversation_new, 'click', () => {
			if (this.is_scratchpad_active()) {
				this.#conversations_list?.index_new?.(this.#storage.CONVERSATION_TYPE_SCRATCHPAD);
			} else {
				this.#conversations_list?.index_new?.(this.#storage.CONVERSATION_TYPE_CHAT);
			}
		});

		addSafeEventListener(this.btn_conversation_list_new, 'click', this.close_conversation.bind(this));
		addSafeEventListener(this.btn_empty_new_chat, 'click', () => this.#conversations_list?.index_new?.(this.#storage.CONVERSATION_TYPE_CHAT));
		addSafeEventListener(this.btn_empty_new_notebook, 'click', () => this.#conversations_list?.index_new?.(this.#storage.CONVERSATION_TYPE_NOTEBOOK));
		addSafeEventListener(this.btn_empty_new_scratchpad, 'click', () => this.#conversations_list?.index_new?.(this.#storage.CONVERSATION_TYPE_SCRATCHPAD));

		addSafeEventListener(this.btn_show_conversations_new, 'click', this.toggle_conversation_panel.bind(this));
		addSafeEventListener(this.btn_show_scratchpads_new, 'click', () => {
			if (window.innerWidth <= this.BREAKPOINT_TABLET) {
				this.state_v_open = false;
				this.state_i_open = true;
			} else {
				this.state_i_open = true;
			}
			this.apply_panels_layout();
			this.#storage.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, 'scratchpad');
			this.on_conversation_updated();
		});
		addSafeEventListener(this.btn_conversations, 'click', this.toggle_conversation_panel.bind(this));
		addSafeEventListener(this.btn_conversation_index, 'click', this.toggle_conversation_index.bind(this));
		addSafeEventListener(this.btn_close, 'click', this.close_conversation.bind(this));

		addSafeEventListener(this.btn_select_conversations, 'click', () => this.#conversations_list?.toggle_conversation_selection_mode?.());
		addSafeEventListener(this.btn_archive_conversations, 'click', () => this.#conversations_list?.archive_selected_conversations?.());
		addSafeEventListener(this.btn_archives_toggle, 'click', () => this.#conversations_list?.toggle_archives?.());
		addSafeEventListener(this.btn_delete_conversations, 'click', () => this.#conversations_list?.delete_selected_conversations?.());

		addSafeEventListener(this.btn_conversation_index_select, 'click', () => {
			if (this.is_scratchpad_active()) {
				this.#scratchpad_index.toggle_response_selection_mode();
			} else {
				this.#conversation_index.toggle_response_selection_mode();
			}
		});

		addSafeEventListener(this.btn_conversation_index_favorite, 'click', () => {
			if (this.is_scratchpad_active()) {
				this.#scratchpad_index.bookmark_selected_responses();
			} else {
				this.#conversation_index.bookmark_selected_responses();
			}
		});

		addSafeEventListener(this.btn_conversation_index_delete, 'click', () => {
			if (this.is_scratchpad_active()) {
				this.#scratchpad_index.delete_selected_responses();
			} else {
				this.#conversation_index.delete_selected_responses();
			}
		});

		addSafeEventListener(this.btn_instant_answer, 'click', this.show_instant_answer.bind(this));
		addSafeEventListener(this.btn_instant_answer_close, 'click', this.hide_instant_answer.bind(this));
		addSafeEventListener(this.btn_instant_answer_send, 'click', this.send_instant_answer.bind(this));
		addSafeEventListener(this.select_instant_answer_model, 'change', () => {
			this.#storage.update_app_config(this.#storage.KEY_CONFIG_INSTANT_ANSWER_MODEL, this.select_instant_answer_model.value);
			this.update_instant_answer_search_button();
			this.update_instant_answer_model_cost_display();
		});
		addSafeEventListener(this.btn_instant_answer_search, 'click', () => this.toggle_instant_answer_search());
		addSafeEventListener(this.instant_answer_query, 'keydown', this.handle_instant_answer_keydown.bind(this));
		addSafeEventListener(this.instant_answer_query, 'input', () => this.resize_instant_answer_textarea());
		addSafeEventListener(this.div_instant_answer_overlay, 'click', (e) => {
			if (e.target === this.div_instant_answer_overlay) {
				this.hide_instant_answer();
			}
		});
		addSafeEventListener(this.btn_app_options, 'click', this.show_app_options.bind(this));
		addSafeEventListener(this.btn_conversation_options, 'click', this.show_conversation_options.bind(this));
		addSafeEventListener(this.btn_options_close, 'click', this.hide_app_options.bind(this));
		addSafeEventListener(this.btn_conversation_options_close, 'click', this.hide_conversation_options.bind(this));
		addSafeEventListener(this.div_app_options_overlay, 'click', (e) => {
			if (e.target === this.div_app_options_overlay) {
				this.hide_app_options();
			}
		});
		addSafeEventListener(this.div_conversation_options_overlay, 'click', (e) => {
			if (e.target === this.div_conversation_options_overlay) {
				this.hide_conversation_options();
			}
		});
		addSafeEventListener(this.btn_show_changelog, 'click', () => this.show_version_overlay());
		addSafeEventListener(this.btn_version_close, 'click', () => this.hide_version_overlay());
		addSafeEventListener(this.div_version_overlay, 'click', (e) => {
			if (e.target === this.div_version_overlay) {
				this.hide_version_overlay();
			}
		});
		addSafeEventListener(this.div_dialog_backdrop, 'click', this.handle_backdrop_click.bind(this));

		window.addEventListener('keydown', (e) => {
			if (e.key === 'Escape') {
				if (this.div_version_overlay && this.div_version_overlay.classList.contains('active')) {
					this.hide_version_overlay();
				} else if (this.div_instant_answer_overlay && this.div_instant_answer_overlay.classList.contains('active')) {
					this.hide_instant_answer();
				} else if (this.div_conversation_options_overlay && this.div_conversation_options_overlay.classList.contains('active')) {
					this.hide_conversation_options();
				} else if (this.div_app_options_overlay && this.div_app_options_overlay.classList.contains('active')) {
					this.hide_app_options();
				}
			}
		});
		addSafeEventListener(this.span_subtitle_toggle, 'click', this.toggle_subtitle.bind(this));
		addSafeEventListener(this.div_title, 'click', this.toggle_subtitle.bind(this));

		addSafeEventListener(this.btn_tab_conversation, 'click', () => this.set_active_tab('conversation'));
		addSafeEventListener(this.btn_tab_context, 'click', () => this.set_active_tab('context'));
		addSafeEventListener(this.btn_tab_memory, 'click', () => this.set_active_tab('memory'));
		addSafeEventListener(this.btn_tab_document, 'click', () => this.set_active_tab('document'));
		addSafeEventListener(this.btn_tab_diction, 'click', () => this.set_active_tab('diction'));
		addSafeEventListener(this.btn_tab_notebook_memory, 'click', () => this.set_active_tab('notebook-memory'));

		addSafeEventListener(this.span_option_model, 'click', this.show_conversation_options.bind(this));
		addSafeEventListener(this.span_option_verbosity, 'click', this.show_conversation_options.bind(this));
		addSafeEventListener(this.span_option_suggested, 'click', () => this._toggle_conversation_option(this.#storage.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES));
		addSafeEventListener(this.span_option_related, 'click', () => this._toggle_conversation_option(this.#storage.KEY_CONVERSATION_SHOW_RELATED_QUERIES));
		addSafeEventListener(this.span_option_search, 'click', () => this._toggle_conversation_option(this.#storage.KEY_CONVERSATION_GOOGLE_SEARCH));
		addSafeEventListener(this.span_option_enforce_topics, 'click', () => this._toggle_conversation_option(this.#storage.KEY_CONVERSATION_ENFORCE_TOPICS));
		addSafeEventListener(this.span_option_auto_run, 'click', () => this._toggle_conversation_option(this.#storage.KEY_CONVERSATION_AUTO_RUN_PROPOSED_QUERIES));
	}

	/**
	 * Handles keydown events in the prompt textarea.
	 * @param {KeyboardEvent} e - The keyboard event object.
	 */
	handle_keydown(e) {
		if (e.ctrlKey && e.key === 'Enter') {
			e.preventDefault();
			if (!this.btn_send || !this.btn_send.disabled) {
				this.send();
			}
		}
	}

	//endregion

	//region Configuration & Local Storage

	/**
	 * Responds to localStorage changes, ensuring application state remains synced across different browser tabs.
	 * @param {StorageEvent} event - The storage event object containing change details.
	 */
	handle_storage_change(event) {
		const config = this.#storage.get_app_config();
		const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];

		if (event.key === this.#storage.KEY_APP_CONFIG) {
			this.on_app_config();
		}
		if (event.key === this.#storage.KEY_APP_INDEX) {
			this.#conversations_list?.on_app_index_updated?.();
		}
		if (event.key === this.#storage.KEY_CONFIG_DEFAULTS) {
			this.apply_app_defaults();
		}
		if (event.key === this.#storage.KEY_APP_CONVERSATION_PREFIX + selected_guid) {
			if (!selected_guid || !this.#api.is_request_active(selected_guid)) {
				this.on_conversation_updated();
			}
		}
	}

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

		const config = this.#storage.get_app_config();
		const experimental_features_enabled = config[this.#storage.KEY_CONFIG_EXPERIMENTAL_FEATURES] || false;
		if (this.btn_empty_new_notebook) {
			this.btn_empty_new_notebook.style.display = experimental_features_enabled ? 'inline-block' : 'none';
		}
		const notebookRow = getEl('id-div-empty-notebook-row');
		if (notebookRow) {
			notebookRow.style.display = experimental_features_enabled ? 'flex' : 'none';
		}

		if (!experimental_features_enabled && (this.state_active_tab === 'memory' || this.state_active_tab === 'notebook-memory')) {
			this.set_active_tab('conversation');
		}
	}

	/**
	 * 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.#storage.KEY_CONFIG_THEME;
		} else if (e.target.id === 'id-select-utility-model') {
			key = this.#storage.KEY_CONFIG_UTILITY_MODEL;
		}
		if (key) {
			this.#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.#storage.get_app_config();
		const currentTheme = config?.[this.#storage.KEY_CONFIG_THEME];
		let newThemeKey = family.themes[0].key;
		if (family.themes.some(t => t.key === currentTheme)) {
			newThemeKey = currentTheme;
		}

		if (this.select_theme) {
			populateThemeDropdown(this.select_theme, themeIndex, familyKey, newThemeKey);
		}
		this.#storage.update_app_config(this.#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.#storage.update_app_config(this.#storage.KEY_CONFIG_EXPERIMENTAL_FEATURES, e.target.checked);
	}

	/**
	 * Shows or hides the experimental sub-options depending on the experimental features setting.
	 */
	update_experimental_options_visibility() {
		if (!this.div_experimental_options) return;
		const config = this.#storage.get_app_config();
		const experimental_features_enabled = config[this.#storage.KEY_CONFIG_EXPERIMENTAL_FEATURES] || false;
		this.div_experimental_options.style.display = experimental_features_enabled ? 'block' : 'none';
	}

	/**
	 * Updates the experimental Mistral provider setting in the application configuration.
	 * @param {Event} e - The change event from the checkbox.
	 */
	update_experimental_mistral_setting(e) {
		this.#storage.update_app_config(this.#storage.KEY_CONFIG_ENABLE_MISTRAL, e.target.checked);
	}

	/**
	 * Updates the experimental Organizer setting in the application configuration.
	 * @param {Event} e - The change event from the checkbox.
	 */
	update_experimental_organizer_setting(e) {
		this.#storage.update_app_config(this.#storage.KEY_CONFIG_ORGANIZER, e.target.checked);
	}

	/**
	 * Updates background keep alive setting in the application configuration.
	 * @param {Event} e - The change event from the checkbox.
	 */
	update_background_keep_alive_setting(e) {
		this.#storage.update_app_config(this.#storage.KEY_CONFIG_BACKGROUND_KEEP_ALIVE, e.target.checked);
	}

	/**
	 * 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.#storage.get_app_config();
		const themeIndex = await fetchThemeIndex();
		const allKeys = getAllThemeKeys(themeIndex);

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

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

		const familyKey = getThemeFamilyForTheme(theme, themeIndex);

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

		applyTheme(theme, true);

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

	/**
	 * Updates the estimated cost display for the selected utility model based on two average sentences.
	 */
	update_utility_model_cost_display() {
		if (!this.div_utility_model_cost) return;
		const utilityKey = this.select_utility_model?.value || 'gemini-3.1-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.div_utility_model_cost.innerHTML = `<span class="span-utility-cost-label">Estimated Cost:</span><span class="span-utility-cost-val">${formattedCost}</span>`;
	}

	//endregion

	//region Layout & Responsive

	/**
	 * Adjusts the prompt placeholder text and panel visibility states based on window resize events.
	 * @param {number} lastWidth - The previous window width.
	 * @param {number} currentWidth - The new current window width.
	 */
	handle_responsive_layout(lastWidth, currentWidth) {
		if (this.prompt) {
			if (currentWidth <= this.BREAKPOINT_MOBILE) {
				this.prompt.placeholder = 'Ctrl + \u27a5 to Send';
			} else {
				this.prompt.placeholder = 'Ctrl + Enter to Send';
			}
		}

		const config = this.#storage.get_app_config();
		const isSelected = !!config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];

		// Transitioning from tablet/desktop to mobile
		if (lastWidth > this.BREAKPOINT_MOBILE && currentWidth <= this.BREAKPOINT_MOBILE && isSelected) {
			this.state_v_open = false;
			this.state_i_open = false;
		}

		// Transitioning from mobile to tablet/desktop
		if (lastWidth <= this.BREAKPOINT_MOBILE && currentWidth > this.BREAKPOINT_MOBILE) {
			this.state_v_open = true;
			this.state_i_open = false;
		}

		// Transitioning from desktop to tablet (ensure only one panel open)
		if (lastWidth > this.BREAKPOINT_TABLET && currentWidth <= this.BREAKPOINT_TABLET) {
			if (this.state_v_open && this.state_i_open) {
				this.state_i_open = false;
			}
		}
	}

	/**
	 * Toggles the visibility of the left-side conversations navigation panel.
	 */
	toggle_conversation_panel() {
		if (window.innerWidth <= this.BREAKPOINT_TABLET) {
			this.state_i_open = false;
		}
		this.state_v_open = !this.state_v_open;
		if (this.state_v_open && window.innerWidth <= this.BREAKPOINT_TABLET) {
			this.state_i_open = false;
		}
		this.apply_panels_layout();
	}

	/**
	 * Toggles the visibility of the center conversation index (response list) panel.
	 */
	toggle_conversation_index() {
		if (window.innerWidth <= this.BREAKPOINT_TABLET) {
			this.state_v_open = false;
		}
		this.state_i_open = !this.state_i_open;
		if (this.state_i_open && window.innerWidth <= this.BREAKPOINT_TABLET) {
			this.state_v_open = false;
		}
		this.apply_panels_layout();
	}

	/**
	 * Updates the main layout container's CSS classes to reflect the current open/closed states of side panels.
	 */
	apply_panels_layout() {
		const body = getEl('id-div-structure-body');
		if (!body) return;

		body.classList.remove('div-structure-body-v-i-c', 'div-structure-body-v-c', 'div-structure-body-i-c', 'div-structure-body-c');

		if (this.state_v_open && this.state_i_open) {
			body.classList.add('div-structure-body-v-i-c');
		} else if (this.state_v_open && !this.state_i_open) {
			body.classList.add('div-structure-body-v-c');
		} else if (!this.state_v_open && this.state_i_open) {
			body.classList.add('div-structure-body-i-c');
		} else {
			body.classList.add('div-structure-body-c');
		}

		if (this.btn_conversations) {
			toggleElementClass(this.btn_conversations, 'underlined-button', !!this.state_v_open);
		}
		if (this.btn_conversation_index) {
			toggleElementClass(this.btn_conversation_index, 'underlined-button', !!this.state_i_open);
		}
	}

	//endregion

	//region Conversation / Chat

	/**
	 * Sets the active tab and updates the UI practicalities.
	 * @param {string} tabName - The name of the tab to activate.
	 */
	set_active_tab(tabName) {
		const conversation = this.get_selected_conversation();
		const type = conversation?.[this.#storage.KEY_CONVERSATION_TYPE] || this.#storage.CONVERSATION_TYPE_CHAT;

		const config = this.#storage.get_app_config();
		const experimental_features_enabled = config[this.#storage.KEY_CONFIG_EXPERIMENTAL_FEATURES] || false;

		this.state_active_tab = tabName;
		this._update_tab_visibility(tabName, type, experimental_features_enabled);

		const tabs = {
			conversation: { render: () => this.render_conversation_tab(false) },
			context: { render: this.render_context_tab.bind(this) },
			memory: { render: this.render_memory_tab.bind(this) },
			document: { render: this.render_document_tab.bind(this) },
			diction: { render: this.render_diction_tab.bind(this) },
			'notebook-memory': { render: this.render_memory_tab.bind(this) }
		};

		if (tabs[tabName] && typeof tabs[tabName].render === 'function') {
			tabs[tabName].render();
		}
	}

	/**
	 * Renders the content for the context tab.
	 */
	render_context_tab() {
		if (!this.div_chat_context_scroll || !this.#context) return;
		this.div_chat_context_scroll.innerHTML = this.#context.render();
		this.#context.attachEventListeners();
	}

	/**
	 * Renders the content for the memory tab.
	 */
	render_memory_tab() {
		const conversation = this.get_selected_conversation();
		const type = conversation?.[this.#storage.KEY_CONVERSATION_TYPE] || this.#storage.CONVERSATION_TYPE_CHAT;
		if (type === this.#storage.CONVERSATION_TYPE_CHAT) {
			if (this.div_chat_memory_scroll) {
				this.div_chat_memory_scroll.innerHTML = this.#memory.render();
			}
		} else if (this.div_notebook_memory_scroll) {
			this.div_notebook_memory_scroll.innerHTML = this.#memory.render();
		}
	}

	/**
	 * Placeholder method for reformatting the active conversation.
	 */
	reformat_conversation() {
		// Reserved for full conversation reformat if needed
	}

	/**
	 * Renders the content for the document tab and binds change listener.
	 */
	render_document_tab() {
		if (!this.div_chat_document_scroll || !this.#document) return;
		const conversation = this.get_selected_conversation();
		const currentDocument = conversation ? conversation[this.#storage.KEY_CONVERSATION_DOCUMENT] || '' : '';
		this.div_chat_document_scroll.innerHTML = this.#document.render(currentDocument);
		this.#document.attachEventListeners((newText) => {
			const selected_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
			if (selected_guid) {
				this.#storage.update_conversation_field(selected_guid, this.#storage.KEY_CONVERSATION_DOCUMENT, newText);
			}
		});
	}

	/**
	 * Renders the content for the diction tab and binds change listener.
	 */
	render_diction_tab() {
		if (!this.div_chat_diction_scroll || !this.#diction) return;
		const conversation = this.get_selected_conversation();
		const currentDiction = conversation ? conversation[this.#storage.KEY_CONVERSATION_DICTION] || '' : '';
		this.div_chat_diction_scroll.innerHTML = this.#diction.render(currentDiction);
		this.#diction.attachEventListeners((newDiction) => {
			const selected_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
			if (selected_guid) {
				this.#storage.update_conversation_field(selected_guid, this.#storage.KEY_CONVERSATION_DICTION, newDiction);
			}
		});
	}

	/**
	 * Renders the chat interface header, displaying the title and summary of the selected conversation.
	 */
	render_conversation_header() {
		const conversation = this.get_selected_conversation();
		const history = conversation?.[this.#storage.KEY_CONVERSATION_HISTORY] || [];

		let title = conversation?.[this.#storage.KEY_CONVERSATION_TITLE] || 'New Conversation';
		let summary = conversation?.[this.#storage.KEY_CONVERSATION_SUMMARY] || '';

		if (history.length > 0) {
			for (let i = history.length - 1; i >= 0; i--) {
				if (history[i] && (history[i].title || history[i].conversationTitle || history[i]['summary'] || history[i]['conversationSummary'])) {
					title = history[i].title || history[i].conversationTitle || title;
					summary = history[i]['summary'] || history[i]['conversationSummary'] || summary;
					break;
				}
			}
		}

		if (this.div_title) {
			let h4_title = this.div_title.querySelector('h4');
			if (h4_title) {
				h4_title.innerHTML = title;
			} else {
				this.div_title.insertAdjacentHTML('afterbegin', `<h4>${title}</h4>`);
			}
		}

		if (this.div_subtitle) {
			let h6_subtitle = this.div_subtitle.querySelector('h6');
			if (h6_subtitle) {
				h6_subtitle.innerHTML = summary;
			} else {
				this.div_subtitle.insertAdjacentHTML('afterbegin', `<h6>${summary}</h6>`);
			}
		}
	}

	/**
	 * Toggles the visibility of the subtitle and rotates the toggle icon.
	 */
	toggle_subtitle() {
		if (!this.div_subtitle) return;
		const is_shown = this.div_subtitle.classList.toggle('subtitle-shown');
		if (this.span_subtitle_toggle) {
			this.span_subtitle_toggle.classList.toggle('rotated', is_shown);
		}
		this.div_subtitle.style.maxHeight = is_shown ? this.div_subtitle.scrollHeight + 'px' : null;
	}

	/**
	 * Cancels any pending scheduled scroll operation.
	 */
	cancel_scroll() {
		if (this.#scrollTimeout) {
			clearTimeout(this.#scrollTimeout);
			this.#scrollTimeout = null;
		}
	}

	/**
	 * Scrolls the chat container to the most recent response in the history.
	 */
	scroll_to_last_item() {
		this.cancel_scroll();
		const config = this.#storage.get_app_config();
		if (config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID]) {
			const history = this.get_selected_conversation()?.[this.#storage.KEY_CONVERSATION_HISTORY];
			if (history && history.length > 0) {
				const lastIndex = history.length - 1;
				const lastItem = history[lastIndex];
				this.#scrollTimeout = setTimeout(() => {
					this.#scrollTimeout = null;
					if (lastItem && lastItem.pending) {
						const container = this.div_chat_container_scroll;
						if (container) {
							container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
						}
					} else {
						const lastEl = getEl('chat-item-' + lastIndex);
						if (lastEl) {
							lastEl.scrollIntoView({ behavior: 'smooth' });
						}
					}
				}, this.SCROLL_DELAY);
			}
		}
	}

	/**
	 * Deselects the current conversation, closes associated panels, and resets the interface.
	 */
	close_conversation() {
		this.state_i_open = false;
		this.state_v_open = window.innerWidth > this.BREAKPOINT_MOBILE;
		this.#storage.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, '');
		this.apply_panels_layout();
		this.on_conversation_updated();
	}

	/**
	 * Returns the count of scratchpad conversations in the index.
	 * @returns {number} The count of scratchpad conversations.
	 */
	get_scratchpad_count() {
		const index = this.#storage.get_app_index();
		let count = 0;
		(index || []).forEach(item => {
			const guid = item[this.#storage.KEY_INDEX_GUID];
			const conversation = this.#storage.get_conversation(guid);
			const type = conversation?.[this.#storage.KEY_CONVERSATION_TYPE] || item?.[this.#storage.KEY_CONVERSATION_TYPE];
			if (type === this.#storage.CONVERSATION_TYPE_SCRATCHPAD) {
				count++;
			}
		});
		return count;
	}

	/**
	 * Validates the state of the conversation index button.
	 */
	validate_conversation_index_button() {
		if (!this.btn_conversation_index) return;
		if (this.is_scratchpad_active()) {
			this.btn_conversation_index.textContent = 'Scratchpads';
			const count = this.get_scratchpad_count();
			this.btn_conversation_index.disabled = (count === 0);
			if (count === 0 && this.state_i_open) {
				this.state_i_open = false;
				this.apply_panels_layout();
			}
		} else {
			this.btn_conversation_index.textContent = 'History';
			const count = this.get_conversation_item_count();
			this.btn_conversation_index.disabled = (count === 0);
			if (count === 0 && this.state_i_open) {
				this.state_i_open = false;
				this.apply_panels_layout();
			}
		}
	}

	/**
	 * Returns the number of responses in the currently selected conversation.
	 * @returns {number} The count of history items.
	 */
	get_conversation_item_count() {
		const config = this.#storage.get_app_config();
		const guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
		if (guid) {
			const conversation = this.#storage.get_conversation(guid);
			if (conversation && conversation[this.#storage.KEY_CONVERSATION_HISTORY]) {
				return conversation[this.#storage.KEY_CONVERSATION_HISTORY].length;
			}
		}
		return 0;
	}

	/**
	 * Checks whether the prompt textarea contains valid text.
	 * @returns {boolean} True if prompt is non-empty and conversation is idle.
	 */
	validate_prompt() {
		if (!this.btn_send) return false;
		const selected_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID] || '';
		const isActive = this.#api.is_request_active(selected_guid);

		if (isActive) {
			this.btn_send.disabled = true;
			this.btn_send.textContent = '...';
			return false;
		}

		this.btn_send.textContent = 'Send';
		if (this.prompt && this.prompt.value && this.prompt.value.trim().length > 0) {
			this.btn_send.disabled = false;
			return true;
		} else {
			this.btn_send.disabled = true;
			return false;
		}
	}

	/**
	 * Adjusts the height of the prompt textarea to match its content.
	 */
	resize_prompt_textarea() {
		if (!this.prompt) return;
		this.prompt.style.height = 'auto';
		this.prompt.style.height = this.prompt.scrollHeight + 'px';
	}

	/**
	 * Checks if any scratchpad conversation currently has an active API request running.
	 * @returns {boolean}
	 * @private
	 */
	_is_any_scratchpad_loading() {
		const activeRequests = this.#api.get_active_requests();
		return activeRequests.some(req => {
			if (!req.guid) return false;
			const conv = this.#storage.get_conversation(req.guid);
			return conv && conv[this.#storage.KEY_CONVERSATION_TYPE] === this.#storage.CONVERSATION_TYPE_SCRATCHPAD;
		});
	}

	/**
	 * Updates the loading indicator for a conversation or all conversations across the UI.
	 * @param {string|null} [guid=null] - Conversation GUID.
	 * @param {boolean|null} [isLoading=null] - Explicit loading state.
	 */
	update_conversation_loading_indicators(guid = null, isLoading = null) {
		if (guid) {
			const active = (isLoading !== null) ? Boolean(isLoading) : this.#api.is_request_active(guid);
			this.#conversations_list?.set_conversation_loading?.(guid, active);
			this.#scratchpad_index?.set_scratchpad_loading?.(guid, active);
		} else {
			const index = this.#storage.get_app_index() || [];
			index.forEach(item => {
				const g = item[this.#storage.KEY_INDEX_GUID];
				if (g) {
					const active = this.#api.is_request_active(g);
					this.#conversations_list?.set_conversation_loading?.(g, active);
					this.#scratchpad_index?.set_scratchpad_loading?.(g, active);
				}
			});
		}
		const anyScratchpadLoading = this._is_any_scratchpad_loading();
		this.#conversations_list?.set_scratchpad_root_loading?.(anyScratchpadLoading);
	}

	/**
	 * Computes the total byte count of thinking data for the pending turn of a conversation from storage.
	 * @param {string} guid - Conversation GUID.
	 * @returns {number} Sum of bytes in thinking content.
	 */
	get_pending_thinking_bytes(guid) {
		if (!guid) return 0;
		const conversation = this.#storage.get_conversation(guid);
		const history = conversation?.[this.#storage.KEY_CONVERSATION_HISTORY];
		if (!Array.isArray(history) || history.length === 0) return 0;

		const pendingItem = history.find(item => item && item.pending) || history[history.length - 1];
		if (!pendingItem || !pendingItem.pending || !pendingItem.thinking) return 0;

		if (Array.isArray(pendingItem.thinking)) {
			return pendingItem.thinking.reduce((total, line) => {
				if (typeof line === 'string') {
					return total + (new TextEncoder().encode(line).length + 1);
				}
				return total;
			}, 0);
		} else if (typeof pendingItem.thinking === 'string') {
			return new TextEncoder().encode(pendingItem.thinking).length;
		}
		return 0;
	}

	/**
	 * Prepares the payload and initiates sending a prompt to the API.
	 */
	send() {
		if (!this.validate_prompt()) {
			return;
		}

		if (this.btn_send) {
			this.btn_send.disabled = true;
			this.btn_send.textContent = '...';
		}

		const query = this.prompt.value;
		const config = this.#storage.get_app_config();
		let guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];

		if (!guid || guid === 'scratchpad') {
			const defaultType = (guid === 'scratchpad')
				? this.#storage.CONVERSATION_TYPE_SCRATCHPAD
				: this.#storage.CONVERSATION_TYPE_CHAT;
			guid = this.#storage.update_app_index('', true, defaultType);
			if (this.#prompt_drafts['']) {
				this.#prompt_drafts[guid] = this.#prompt_drafts[''];
				delete this.#prompt_drafts[''];
			} else {
				this.#prompt_drafts[guid] = query;
			}
			this.#active_conversation_guid = guid;
			this.#storage.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, guid);
		} else {
			this.#prompt_drafts[guid] = query;
		}

		const context = [];
		if (guid) {
			const conversation = this.#storage.get_conversation(guid);
			let conversationHistory = conversation[this.#storage.KEY_CONVERSATION_HISTORY] || [];

			const wasEmpty = (conversationHistory.filter(item => !item.pending).length === 0);
			conversationHistory = conversationHistory.filter(item => !item.pending && this._has_valid_content(item));
			conversationHistory.push({
				query: query,
				pending: true,
				thinking: []
			});

			conversation[this.#storage.KEY_CONVERSATION_HISTORY] = conversationHistory;
			this.#storage.save_conversation(guid, conversation);

			if (wasEmpty && window.innerWidth > this.BREAKPOINT_MOBILE) {
				if (!this.state_v_open) {
					this.state_i_open = true;
					this.apply_panels_layout();
				}
			}

			this.on_conversation_updated();

			const history = conversation[this.#storage.KEY_CONVERSATION_HISTORY];
			if (history && history.length > 1) {
				const isScratchpad = conversation[this.#storage.KEY_CONVERSATION_TYPE] === this.#storage.CONVERSATION_TYPE_SCRATCHPAD;
				const max_full_values = isScratchpad ? 1 : 3;
				const max_summaries = isScratchpad ? 0 : 6;
				let fullValueCount = 0;
				let summaryCount = 0;
				let chainBroken = false;

				for (let i = history.length - 2; i >= 0; i--) {
					const item = history[i];
					if (item.pending) continue;
					const prompt = item.query;
					let reply = '';

					if (!chainBroken && fullValueCount < max_full_values) {
						reply = (item.content || []).map(c => {
							if (c.type === 'table' && c['table-rows']) {
								return c['table-rows'].map(row => row.join(' | ')).join('\n');
							}
							return c.value || '';
						}).join('\n ');
						fullValueCount++;
					} else if (summaryCount < max_summaries) {
						reply = item['summary'] || '';
						summaryCount++;
					} else {
						break;
					}

					context.unshift({
						prompt: prompt,
						reply: reply
					});

					if (item.chain === false) {
						chainBroken = true;
					}
				}
			}
		}

		const selected_conversation = this.#storage.get_selected_conversation();
		const app_defaults = this.#storage.get_app_defaults();
		const verbosity = selected_conversation?.[this.#storage.KEY_CONVERSATION_VERBOSITY] || app_defaults?.[this.#storage.KEY_CONFIG_DEFAULT_VERBOSITY] || 'standard';
		const family = selected_conversation?.[this.#storage.KEY_CONVERSATION_MODEL_FAMILY] || app_defaults?.[this.#storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY] || 'gemini';
		const model_version = selected_conversation?.[this.#storage.KEY_CONVERSATION_MODEL_VERSION] || app_defaults?.[this.#storage.KEY_CONFIG_DEFAULT_MODEL_VERSION] || 'gemini-3.5-flash-lite';
		const thinking_level = (selected_conversation && selected_conversation[this.#storage.KEY_CONVERSATION_THINKING_LEVEL] !== undefined)
			? selected_conversation[this.#storage.KEY_CONVERSATION_THINKING_LEVEL]
			: (app_defaults?.[this.#storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL] ?? 'MINIMAL');
		const has_explicit_model = !!(selected_conversation?.[this.#storage.KEY_CONVERSATION_MODEL_FAMILY] || selected_conversation?.[this.#storage.KEY_CONVERSATION_MODEL_VERSION]);
		let model_key = selected_conversation?.[this.#storage.KEY_CONVERSATION_MODEL];
		if (model_key === undefined || model_key === null || model_key === '') {
			model_key = has_explicit_model ? (model_version || '') : (app_defaults?.[this.#storage.KEY_CONFIG_DEFAULT_MODEL] || '');
		}
		const meta_context = {};

		if (selected_conversation && selected_conversation[this.#storage.KEY_CONVERSATION_TOPICS]) {
			meta_context.topics = selected_conversation[this.#storage.KEY_CONVERSATION_TOPICS];
		}
		if (selected_conversation && selected_conversation[this.#storage.KEY_CONVERSATION_CONSIDERATIONS]) {
			meta_context.considerations = selected_conversation[this.#storage.KEY_CONVERSATION_CONSIDERATIONS];
		}
		if (selected_conversation && selected_conversation[this.#storage.KEY_CONVERSATION_GOOGLE_SEARCH] && this._model_supports_web_search(model_version)) {
			meta_context.google_search = selected_conversation[this.#storage.KEY_CONVERSATION_GOOGLE_SEARCH];
		}

		meta_context.enforce_topics = selected_conversation?.[this.#storage.KEY_CONVERSATION_ENFORCE_TOPICS] || app_defaults?.[this.#storage.KEY_CONFIG_ENFORCE_TOPICS] || false;

		this.show_progress_ui(guid);
		void this.start_background_keep_alive();
		this.update_conversation_loading_indicators(guid, true);

		const isScratchpad = selected_conversation && selected_conversation[this.#storage.KEY_CONVERSATION_TYPE] === this.#storage.CONVERSATION_TYPE_SCRATCHPAD;
		let function_name = 'chat';
		if (isScratchpad) {
			function_name = 'chat&scratchpad=true';
		}

		const payload = this.#api.format_data(
			query,
			context,
			verbosity,
			model_key,
			meta_context,
			false,
			{ family, model: model_version, thinking: thinking_level }
		);

		if (isScratchpad) {
			payload.scratchpad = true;
		}

		void this.#api.post(
			payload,
			(response, targetGuid) => {
				this.send_success(response, query, targetGuid || guid);
			},
			(response, targetGuid) => {
				this.send_failure(response, targetGuid || guid);
			},
			function_name,
			null,
			(bytes, targetGuid) => this.update_progress(bytes, targetGuid || guid),
			guid
		);
	}

	/**
	 * Displays the progress indicator UI and stop button during request processing.
	 * @param {string|null} [guid=null] - Optional conversation GUID.
	 */
	show_progress_ui(guid = null) {
		setElementDisplay(getEl('div-prompt-input'), 'none');
		setElementDisplay(getEl('div-prompt-clarification'), 'none');

		const progressDiv = getEl('div-response-progress');
		if (!progressDiv) return;
		progressDiv.innerHTML = '';
		setElementDisplay(progressDiv, 'flex');

		const current_guid = guid || this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID] || '';
		const bytes = this.get_pending_thinking_bytes(current_guid);

		const text = document.createElement('span');
		text.id = 'span-progress-text';
		text.textContent = bytes > 0 ? `Processing response... (${bytes})` : 'Processing response...';
		progressDiv.appendChild(text);

		const stopBtn = document.createElement('button');
		stopBtn.id = 'btn-stop-response';
		stopBtn.textContent = 'Stop';
		stopBtn.className = 'as-icon';
		stopBtn.style.float = 'right';
		stopBtn.onclick = () => this.stop_response(current_guid);
		progressDiv.appendChild(stopBtn);

		if (this.btn_send) {
			this.btn_send.disabled = true;
			this.btn_send.textContent = '...';
		}
	}

	/**
	 * Updates the response progress label with received byte count.
	 * @param {number} bytes - Bytes received.
	 * @param {string} [guid] - Conversation GUID.
	 */
	update_progress(bytes, guid) {
		const current_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID] || '';
		if (guid && guid !== current_guid) return;
		const text = getEl('span-progress-text');
		if (text) {
			const displayBytes = (bytes !== undefined && bytes !== null && bytes > 0)
				? bytes
				: this.get_pending_thinking_bytes(guid || current_guid);
			text.textContent = displayBytes > 0 ? `Processing response... (${displayBytes})` : 'Processing response...';
		}
	}

	/**
	 * Hides the progress UI and restores the prompt input area.
	 */
	hide_progress_ui() {
		setElementDisplay(getEl('div-prompt-input'), 'block');
		setElementDisplay(getEl('div-prompt-clarification'), 'none');
		setElementDisplay(getEl('div-response-progress'), 'none');

		if (this.btn_send) {
			this.btn_send.textContent = 'Send';
			this.btn_send.disabled = !this.validate_prompt();
		}
	}

	/**
	 * Acquires a screen wake lock and initiates silent audio context to prevent background throttling.
	 * Only runs when both experimental features and background keep-alive are enabled.
	 * @returns {Promise<void>}
	 */
	async start_background_keep_alive() {
		const config = this.#storage.get_app_config();
		const experimental_features_enabled = config[this.#storage.KEY_CONFIG_EXPERIMENTAL_FEATURES] || false;
		const keep_alive_enabled = config[this.#storage.KEY_CONFIG_BACKGROUND_KEEP_ALIVE] || false;
		if (!experimental_features_enabled || !keep_alive_enabled) return;

		try {
			if ('wakeLock' in navigator) {
				this.#wakeLock = await navigator.wakeLock.request('screen');
			}
		} catch (err) {
			console.warn('Wake Lock request failed:', err);
		}

		try {
			const AudioContextClass = window.AudioContext || window['webkitAudioContext'];
			if (AudioContextClass) {
				this.#audioCtx = new AudioContextClass();
				const oscillator = this.#audioCtx.createOscillator();
				const gainNode = this.#audioCtx.createGain();
				gainNode.gain.value = 0.0;
				oscillator.connect(gainNode);
				gainNode.connect(this.#audioCtx.destination);
				oscillator.start();
			}
		} catch (e) {
			console.warn('Audio Context keep-alive failed:', e);
		}
	}

	/**
	 * Releases wake lock and closes the background audio context only when no active requests remain.
	 */
	stop_background_keep_alive() {
		if (this.#api && this.#api.is_request_active()) {
			return;
		}

		if (this.#wakeLock) {
			this.#wakeLock.release().then(() => {
				this.#wakeLock = null;
			}).catch(() => {
				this.#wakeLock = null;
			});
		}

		if (this.#audioCtx) {
			this.#audioCtx.close().then(() => {
				this.#audioCtx = null;
			}).catch(() => {
				this.#audioCtx = null;
			});
		}
	}

	/**
	 * Aborts the active API request for the specified GUID (or currently selected conversation) and resets UI.
	 * @param {string} [targetGuid] - Optional GUID to abort.
	 */
	stop_response(targetGuid) {
		const selected_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID] || '';
		const guid = targetGuid || selected_guid;

		let stoppedPrompt = '';
		if (guid) {
			const conversation = this.#storage.get_conversation(guid);
			const history = conversation?.[this.#storage.KEY_CONVERSATION_HISTORY];
			if (Array.isArray(history) && history.length > 0 && history[history.length - 1].pending) {
				const pendingItem = history.pop();
				if (pendingItem && pendingItem.query) {
					stoppedPrompt = pendingItem.query;
				}
				conversation[this.#storage.KEY_CONVERSATION_HISTORY] = history;
				this.#storage.save_conversation(guid, conversation);
			}

			if (stoppedPrompt) {
				this.#prompt_drafts[guid] = stoppedPrompt;
			} else if (this.#prompt_drafts[guid]) {
				stoppedPrompt = this.#prompt_drafts[guid];
			}

			this.#api.abort_request(guid);
		} else {
			this.#api.abort_all();
		}

		if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function') {
			window.dispatchEvent(new CustomEvent('tinai:stop', {
				detail: { guid, prompt: stoppedPrompt }
			}));
			window.dispatchEvent(new CustomEvent('tinai:request-stop', {
				detail: { guid, prompt: stoppedPrompt }
			}));
		}

		this.stop_background_keep_alive();
		this.update_conversation_loading_indicators(guid, false);

		if (!guid || guid === selected_guid) {
			this.hide_progress_ui();

			if (this.prompt) {
				this.prompt.value = stoppedPrompt || this.#prompt_drafts[guid] || '';
				this.resize_prompt_textarea();
				this.validate_prompt();
				this.prompt.focus();
			}

			if (this.btn_send) {
				this.btn_send.textContent = 'Send';
				this.btn_send.disabled = !this.validate_prompt();
			}

			this.on_conversation_updated(false);
		} else {
			this.#conversations_list?.on_app_index_updated?.();
			this.#scratchpad_index?.on_conversation_index_updated?.();
		}
	}

	/**
	 * Appends streamed thinking chunks to the currently pending history item for a specific conversation.
	 * @param {string|Array<string>} chunk - Streamed thinking text snippet.
	 * @param {string} [targetGuid] - Conversation GUID.
	 */
	send_thinking(chunk, targetGuid) {
		const current_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
		const guid = targetGuid || current_guid;
		if (!guid) return;

		const conversation = this.#storage.get_conversation(guid);
		const history = conversation?.[this.#storage.KEY_CONVERSATION_HISTORY];
		if (!history || history.length === 0) return;

		let pendingItem = history[history.length - 1];
		if (!pendingItem || !pendingItem.pending) {
			pendingItem = history.find(item => item && item.pending);
			if (!pendingItem) return;
		}

		if (!Array.isArray(pendingItem.thinking)) {
			pendingItem.thinking = pendingItem.thinking ? [pendingItem.thinking] : [];
		}

		if (Array.isArray(chunk)) {
			chunk.forEach(c => {
				if (typeof c === 'string' && c.trim().length > 0) {
					pendingItem.thinking.push(...c.split('\n').map(l => l.trim()).filter(l => l.length > 0));
				}
			});
		} else if (typeof chunk === 'string') {
			const sublines = chunk.split('\n').map(l => l.trim()).filter(l => l.length > 0);
			if (sublines.length > 0) {
				pendingItem.thinking.push(...sublines);
			}
		}

		conversation[this.#storage.KEY_CONVERSATION_HISTORY] = history;
		this.#storage.save_conversation(guid, conversation);

		if (guid === current_guid) {
			const bytes = this.get_pending_thinking_bytes(guid);
			const text = getEl('span-progress-text');
			if (text && bytes > 0) {
				text.textContent = `Processing response... (${bytes})`;
			}

			const lastIndex = history.indexOf(pendingItem);
			let thinkingContainer = this.div_response?.querySelector(`.div-thinking-content[data-index="${lastIndex}"]`);
			if (!thinkingContainer) {
				const allContainers = this.div_response?.querySelectorAll('.div-thinking-content');
				if (allContainers && allContainers.length > 0) {
					thinkingContainer = allContainers[allContainers.length - 1];
				}
			}

			if (thinkingContainer) {
				const textDiv = thinkingContainer.querySelector('.div-thinking-text');
				if (textDiv) {
					const lines = pendingItem.thinking;
					if (lines.length > 0) {
						textDiv.innerHTML = lines.map(line => formatThinkingLineHtml(line)).join('');
					}
					if (!thinkingContainer.classList.contains('thinking-expanded') && !thinkingContainer.classList.contains('thinking-collapsed')) {
						thinkingContainer.classList.add('thinking-expanded');
						textDiv.style.display = 'block';
					} else if (thinkingContainer.classList.contains('thinking-expanded')) {
						textDiv.style.display = 'block';
					}
				}
				thinkingContainer.dispatchEvent(new CustomEvent('thinking-updated', {
					bubbles: true,
					detail: { guid, index: lastIndex, thinking: pendingItem.thinking, chunk }
				}));
				this.scroll_to_last_item();
			} else {
				this.on_conversation_updated(false);
			}
		}
	}

	/**
	 * Callback handling API execution failure or cancellation.
	 * @param {Object} response - The API error response.
	 * @param {string} [targetGuid] - Conversation GUID.
	 */
	send_failure(response, targetGuid) {
		this.stop_background_keep_alive();

		const current_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID] || '';
		const guid = targetGuid || current_guid;

		this.update_conversation_loading_indicators(guid, false);

		let stoppedPrompt = '';
		if (guid) {
			const conversation = this.#storage.get_conversation(guid);
			const history = conversation?.[this.#storage.KEY_CONVERSATION_HISTORY];
			if (history && history.length > 0 && history[history.length - 1].pending) {
				const pendingItem = history.pop();
				if (pendingItem && pendingItem.query) {
					stoppedPrompt = pendingItem.query;
				}
				conversation[this.#storage.KEY_CONVERSATION_HISTORY] = history;
				this.#storage.save_conversation(guid, conversation);
			}
		}

		if (stoppedPrompt) {
			this.#prompt_drafts[guid] = stoppedPrompt;
		}

		if (current_guid === guid || !current_guid) {
			this.hide_progress_ui();

			if (this.prompt) {
				if (stoppedPrompt || this.#prompt_drafts[guid]) {
					this.prompt.value = stoppedPrompt || this.#prompt_drafts[guid] || '';
				}
				this.resize_prompt_textarea();
				this.validate_prompt();
				this.prompt.focus();
			}

			if (this.btn_send) {
				this.btn_send.textContent = 'Send';
				this.btn_send.disabled = !this.validate_prompt();
			}

			this.on_conversation_updated(false);

			if (response && (response.status === 'aborted' || response.name === 'AbortError')) {
				return;
			}

			void customAlert('Error', response?.error || response?.message || 'Something went wrong while communicating with the server.');
		} else {
			this.#conversations_list?.on_app_index_updated?.();
			this.#scratchpad_index?.on_conversation_index_updated?.();
		}
	}

	/**
	 * Plays a subtle sound alert upon receiving a completion response.
	 */
	play_alert_sound() {
		const app_defaults = this.#storage.get_app_defaults();
		const play_chime = app_defaults?.[this.#storage.KEY_CONFIG_PLAY_CHIME] ?? false;
		if (!play_chime) return;

		try {
			const AudioContextClass = window.AudioContext || window['webkitAudioContext'];
			if (!AudioContextClass) return;

			const ctx = new AudioContextClass();
			const now = ctx.currentTime;

			const osc1 = ctx.createOscillator();
			const gain1 = ctx.createGain();
			osc1.type = 'sine';
			osc1.frequency.setValueAtTime(587.33, now); // D5
			gain1.gain.setValueAtTime(0.15, now);
			gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
			osc1.connect(gain1);
			gain1.connect(ctx.destination);
			osc1.start(now);
			osc1.stop(now + 0.15);

			const osc2 = ctx.createOscillator();
			const gain2 = ctx.createGain();
			osc2.type = 'sine';
			osc2.frequency.setValueAtTime(880, now + 0.1); // A5
			gain2.gain.setValueAtTime(0, now);
			gain2.gain.setValueAtTime(0.15, now + 0.1);
			gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
			osc2.connect(gain2);
			gain2.connect(ctx.destination);
			osc2.start(now + 0.1);
			osc2.stop(now + 0.35);
		} catch (e) {
			console.warn('Audio feedback failed:', e);
		}
	}

	/**
	 * Processes successful API responses, updates state, and renders the updated conversation.
	 * @param {Object} response - The API response object.
	 * @param {string} originalQuery - The original user prompt query.
	 * @param {string} [targetGuid] - The conversation GUID the response belongs to.
	 */
	send_success(response, originalQuery, targetGuid) {
		this.stop_background_keep_alive();
		this.play_alert_sound();

		const current_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID] || '';
		const selected_guid = targetGuid || current_guid;

		this.update_conversation_loading_indicators(selected_guid, false);

		if (selected_guid) {
			delete this.#prompt_drafts[selected_guid];
		}
		delete this.#prompt_drafts[''];

		if (current_guid === selected_guid || !current_guid) {
			this.hide_progress_ui();
			if (this.btn_send) {
				this.btn_send.disabled = false;
				this.btn_send.textContent = 'Send';
			}
			if (this.prompt) {
				this.prompt.value = '';
				this.resize_prompt_textarea();
				this.validate_prompt();
				this.prompt.focus();
			}
		}

		if (!selected_guid) return;

		let conversation = this.#storage.get_conversation(selected_guid);
		let history = conversation?.[this.#storage.KEY_CONVERSATION_HISTORY] || [];

		let pendingThinking = null;
		if (history && history.length > 0 && history[history.length - 1].pending) {
			const pendingItem = history.pop();
			if (pendingItem && pendingItem.thinking) {
				pendingThinking = pendingItem.thinking;
			}
		}

		if (response.thinking) {
			if (typeof response.thinking === 'string') {
				response.thinking = response.thinking.split('\n').map(l => l.trim()).filter(l => l.length > 0);
			} else if (Array.isArray(response.thinking)) {
				response.thinking = response.thinking.flatMap(item => typeof item === 'string' ? item.split('\n').map(l => l.trim()).filter(l => l.length > 0) : item);
			}
		} else if (pendingThinking) {
			response.thinking = pendingThinking;
		}

		if (response.title || response.conversationTitle) {
			conversation[this.#storage.KEY_CONVERSATION_TITLE] = response.title || response.conversationTitle;
		}
		if (response.summary || response.conversationSummary) {
			conversation[this.#storage.KEY_CONVERSATION_SUMMARY] = response.summary || response.conversationSummary;
		}
		response.query = originalQuery;
		history.push(response);

		conversation[this.#storage.KEY_CONVERSATION_HISTORY] = history;

		if (response.topics) {
			conversation[this.#storage.KEY_CONVERSATION_TOPICS] = response.topics;
		}
		if (response.considerations) {
			conversation[this.#storage.KEY_CONVERSATION_CONSIDERATIONS] = response.considerations;
		}

		this.#storage.save_conversation(selected_guid, conversation);
		this.#storage.update_app_index(selected_guid);

		if (current_guid === selected_guid || !current_guid) {
			const wasFirstCompletedEntry = (history.length === 1);
			if (wasFirstCompletedEntry && window.innerWidth > this.BREAKPOINT_MOBILE && !this.state_i_open) {
				if (!this.state_v_open) {
					this.state_i_open = true;
					this.apply_panels_layout();
				}
			}

			this.on_conversation_updated();
			if (this.prompt) {
				this.prompt.focus();
			}

			const app_defaults = this.#storage.get_app_defaults();
			const auto_run = (conversation && conversation[this.#storage.KEY_CONVERSATION_AUTO_RUN_PROPOSED_QUERIES] !== undefined)
				? conversation[this.#storage.KEY_CONVERSATION_AUTO_RUN_PROPOSED_QUERIES]
				: (app_defaults?.[this.#storage.KEY_CONFIG_AUTO_RUN_PROPOSED_QUERIES] ?? false);

			if (auto_run && response.proposed_query && response.proposed_query.trim().length > 0) {
				setTimeout(() => {
					const active_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
					if (active_guid === selected_guid) {
						this.send_suggested_query(response.proposed_query);
					}
				}, 1000);
			}
		} else {
			this.#conversations_list?.on_app_index_updated?.();
			this.#scratchpad_index?.on_conversation_index_updated?.();
		}
	}

	/**
	 * Populates prompt textarea with a suggested query and initiates request execution.
	 * @param {string} query - The proposed query string.
	 */
	send_suggested_query(query) {
		if (this.prompt) {
			this.prompt.value = query;
			const config = this.#storage.get_app_config();
			const current_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID] || '';
			this.#prompt_drafts[current_guid] = query;
			this.resize_prompt_textarea();
			this.validate_prompt();
			this.send();
		}
	}

	/**
	 * Renders the conversation tab based on conversation type.
	 * @param {boolean} [scroll_to_last=true] - Whether to auto-scroll to the latest item.
	 * @returns {Promise<void>}
	 */
	async render_conversation_tab(scroll_to_last = true) {
		const conversation = this.get_selected_conversation();
		const type = conversation?.[this.#storage.KEY_CONVERSATION_TYPE] || this.#storage.CONVERSATION_TYPE_CHAT;

		if (type === this.#storage.CONVERSATION_TYPE_SCRATCHPAD) {
			await this.render_scratchpad_conversation(scroll_to_last);
		} else {
			await this.render_chat_conversation(scroll_to_last);
		}
	}

	/**
	 * Renders chat history items for standard conversation.
	 * @param {boolean} [scroll_to_last=true] - Whether to auto-scroll.
	 * @returns {Promise<void>}
	 */
	async render_chat_conversation(scroll_to_last = true) {
		if (!this.div_response) return;

		const conversation = this.get_selected_conversation();
		const app_defaults = this.#storage.get_app_defaults();
		const show_suggested = (conversation && conversation[this.#storage.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES] !== undefined)
			? conversation[this.#storage.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES]
			: (app_defaults?.[this.#storage.KEY_CONFIG_SHOW_SUGGESTED_QUERIES] ?? false);
		const show_related = (conversation && conversation[this.#storage.KEY_CONVERSATION_SHOW_RELATED_QUERIES] !== undefined)
			? conversation[this.#storage.KEY_CONVERSATION_SHOW_RELATED_QUERIES]
			: (app_defaults?.[this.#storage.KEY_CONFIG_SHOW_RELATED_QUERIES] ?? false);

		const options = {
			show_suggested: show_suggested,
			show_related: show_related
		};

		this.div_response.innerHTML = this.#conversation.render(conversation, options);
		await this._post_render_conversation(scroll_to_last);
	}

	/**
	 * Renders conversation items for scratchpad mode.
	 * @param {boolean} [scroll_to_last=true] - Whether to auto-scroll.
	 * @returns {Promise<void>}
	 */
	async render_scratchpad_conversation(scroll_to_last = true) {
		if (!this.div_response) return;

		const conversation = this.get_selected_conversation();
		const app_defaults = this.#storage.get_app_defaults();
		const show_suggested = (conversation && conversation[this.#storage.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES] !== undefined)
			? conversation[this.#storage.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES]
			: (app_defaults?.[this.#storage.KEY_CONFIG_SHOW_SUGGESTED_QUERIES] ?? false);
		const show_related = (conversation && conversation[this.#storage.KEY_CONVERSATION_SHOW_RELATED_QUERIES] !== undefined)
			? conversation[this.#storage.KEY_CONVERSATION_SHOW_RELATED_QUERIES]
			: (app_defaults?.[this.#storage.KEY_CONFIG_SHOW_RELATED_QUERIES] ?? false);

		const options = {
			show_suggested: show_suggested,
			show_related: show_related
		};

		this.div_response.innerHTML = this.#scratchpad_conversation.render(conversation, options);
		await this._post_render_conversation(scroll_to_last);
	}

	/**
	 * Performs post-rendering setup like syntax highlighting, listeners attachment, and scrolling.
	 * @param {boolean} scroll_to_last - Whether to scroll to bottom.
	 * @private
	 */
	/**
	 * Renders Mermaid diagrams in the specified container element.
	 * @param {HTMLElement} container
	 * @private
	 */
	async _render_mermaid_in_container(container) {
		if (typeof mermaid === 'undefined' || !container) return;

		// Convert any pre code.language-mermaid elements into .div-diagram-mermaid containers
		const codeNodes = Array.from(container.querySelectorAll('pre code.language-mermaid:not([data-processed="true"])'));
		for (const codeNode of codeNodes) {
			codeNode.setAttribute('data-processed', 'true');
			const pre = codeNode.parentElement;
			if (!pre) continue;
			const rawCode = codeNode.textContent || '';
			const div = document.createElement('div');
			div.className = 'div-diagram-mermaid';
			div.setAttribute('data-mermaid', rawCode.trim());
			div.textContent = rawCode.trim();
			pre.parentNode.replaceChild(div, pre);
		}

		const mermaidNodes = Array.from(container.querySelectorAll('.mermaid:not([data-processed="true"]), .div-diagram-mermaid:not([data-processed="true"])'))
			.filter(node => !node.querySelector('svg') && (node.getAttribute('data-mermaid') || node.textContent || '').trim().length > 0);

		for (let i = 0; i < mermaidNodes.length; i++) {
			const node = mermaidNodes[i];
			node.setAttribute('data-processed', 'true');
			const rawCode = node.getAttribute('data-mermaid') || node.textContent || '';
			const code = rawCode.trim();
			if (!code) continue;

			const id = 'mermaid_' + Date.now() + '_' + i + '_' + Math.random().toString(36).substring(2, 7);
			try {
				const result = await mermaid.render(id, code);
				if (result && result.svg) {
					node.innerHTML = result.svg;
					if (typeof result.bindFunctions === 'function') {
						result.bindFunctions(node);
					}
				}
			} catch (e) {
				console.warn('Mermaid render error for diagram:', e);
				const errEl = document.getElementById(id) || document.getElementById('d' + id);
				if (errEl) errEl.remove();
				node.innerHTML = `<pre class="code-block language-mermaid"><code>${escapeHtml(code)}</code></pre>`;
			}
		}
	}

	async _post_render_conversation(scroll_to_last) {
		highlightCodeBlocks(this.div_response);
		await this._render_mermaid_in_container(this.div_response);

		if (typeof MathJax !== 'undefined' && MathJax.typesetPromise) {
			try {
				await MathJax.typesetPromise([this.div_response]);
			} catch (e) {
				console.error('MathJax render error:', e);
			}
		}

		this._attach_conversation_listeners();

		if (scroll_to_last) {
			this.scroll_to_last_item();
		}
	}

	/**
	 * Attaches click event listeners to interactive elements inside the conversation response.
	 * @private
	 */
	_attach_conversation_listeners() {
		this.div_response.querySelectorAll('.copyable-code').forEach((btn) => {
			btn.onclick = () => {
				const code = btn.getAttribute('data-code');
				void copyToClipboard(code, btn);
			};
		});

		this.div_response.querySelectorAll('.copyable-table').forEach((btn) => {
			btn.onclick = () => {
				const table = btn.closest('table') || btn.nextElementSibling;
				const tableData = btn.getAttribute('data-table');
				void copyToClipboard(table || tableData, btn);
			};
		});

		this.div_response.querySelectorAll('.btn-copy-response, .btn-copy-scratchpad').forEach((btn) => {
			btn.onclick = () => {
				const index = btn.getAttribute('data-index');
				const responseDiv = this.div_response.querySelector(`.div-response-content[data-index="${index}"]`);
				if (responseDiv) {
					void copyToClipboard(responseDiv, btn);
				}
			};
		});

		this.div_response.querySelectorAll('.btn-redo-response, .btn-redo-scratchpad').forEach((btn) => {
			btn.onclick = async () => {
				const index = parseInt(btn.getAttribute('data-index'), 10);
				const selected_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
				if (selected_guid && !isNaN(index)) {
					const conversation = this.#storage.get_conversation(selected_guid);
					const history = conversation?.[this.#storage.KEY_CONVERSATION_HISTORY];
					if (history && history[index]) {
						const item = history[index];
						const query = item.query || '';
						const summary = item.summary || '';
						let message = `Do you want to Re-send this query?\n\nQuery: "${query}"`;
						if (summary) {
							message += `\n\nSummary: "${summary}"`;
						}
						if (await customConfirm('Re-send Query', message)) {
							history.splice(index);
							conversation[this.#storage.KEY_CONVERSATION_HISTORY] = history;
							this.#storage.save_conversation(selected_guid, conversation);
							this.#storage.update_app_index(selected_guid, false);

							this.prompt.value = query;
							this.#prompt_drafts[selected_guid] = query;
							this.resize_prompt_textarea();
							this.validate_prompt();
							this.send();
						}
					}
				}
			};
		});

		this.div_response.querySelectorAll('.btn-undo-scratchpad').forEach((btn) => {
			btn.onclick = async () => {
				const index = parseInt(btn.getAttribute('data-index'), 10);
				const message = index === 0
					? 'Are you sure you want to remove this scratchpad entry?'
					: 'Are you sure you want to delete the latest item and revert to the previous one?';
				if (await customConfirm('Undo', message)) {
					const selected_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
					if (selected_guid && !isNaN(index)) {
						const conversation = this.#storage.get_conversation(selected_guid);
						const history = conversation?.[this.#storage.KEY_CONVERSATION_HISTORY];
						if (history && history[index]) {
							history.splice(index);
							conversation[this.#storage.KEY_CONVERSATION_HISTORY] = history;
							this.#storage.save_conversation(selected_guid, conversation);
							this.#storage.update_app_index(selected_guid, false);
							this.on_conversation_updated(false);
						}
					}
				}
			};
		});

		this.div_response.querySelectorAll('.btn-delete-response').forEach((btn) => {
			btn.onclick = async () => {
				const index = parseInt(btn.getAttribute('data-index'), 10);
				const selected_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
				if (selected_guid && !isNaN(index)) {
					const conversation = this.#storage.get_conversation(selected_guid);
					const history = conversation?.[this.#storage.KEY_CONVERSATION_HISTORY];
					if (history && history[index]) {
						const item = history[index];
						const summary = item.summary || item.query || 'this response';
						if (await customConfirm('Delete Response', `Are you sure you want to delete: "${summary}"?`)) {
							history.splice(index, 1);
							conversation[this.#storage.KEY_CONVERSATION_HISTORY] = history;
							this.#storage.save_conversation(selected_guid, conversation);
							this.on_conversation_updated(false);
						}
					}
				}
			};
		});

		this.div_response.querySelectorAll('.btn-delete-scratchpad').forEach((btn) => {
			btn.onclick = async () => {
				if (await customConfirm('Delete Scratchpad', 'Are you sure you want to delete this entire scratchpad conversation?')) {
					const config = this.#storage.get_app_config();
					const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
					if (selected_guid) {
						delete this.#prompt_drafts[selected_guid];
						this.#storage.index_delete(selected_guid);
						if (this.#scratchpad_index) {
							this.#scratchpad_index.on_conversation_index_updated();
						}
						const remaining = this.#scratchpad_index ? this.#scratchpad_index.get_scratchpad_conversations() : [];
						if (remaining.length > 0) {
							this.#storage.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, remaining[0][this.#storage.KEY_INDEX_GUID]);
							this.on_conversation_updated(false);
						} else {
							this.close_conversation();
							this.on_conversation_updated(false);
						}
					}
				}
			};
		});

		this.div_response.querySelectorAll('.p-thinking-header').forEach((header) => {
			header.onclick = () => {
				const container = header.closest('.div-thinking-content');
				if (!container) return;
				const isExpanded = container.classList.contains('thinking-expanded');
				const arrow = header.querySelector('.span-thinking-arrow');
				const textDiv = container.querySelector('.div-thinking-text');
				if (isExpanded) {
					container.classList.remove('thinking-expanded');
					container.classList.add('thinking-collapsed');
					if (arrow) arrow.textContent = '\u25b6';
					if (textDiv) textDiv.style.display = 'none';
				} else {
					container.classList.remove('thinking-collapsed');
					container.classList.add('thinking-expanded');
					if (arrow) arrow.textContent = '\u25bc';
					if (textDiv) textDiv.style.display = 'block';
				}
			};
		});

		this.div_response.querySelectorAll('.proposed-query-btn, .related-query-btn, .button-clarification, .span-clickable-query, .span-query-chip').forEach((btn) => {
			btn.onclick = () => {
				const query = btn.getAttribute('data-query') || btn.textContent.trim();
				if (query) {
					this.send_suggested_query(query);
				}
			};
		});

		this.div_response.querySelectorAll('.bookmark-response-btn').forEach((btn) => {
			btn.onclick = () => {
				const index = parseInt(btn.getAttribute('data-index'), 10);
				const selected_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
				if (selected_guid && !isNaN(index)) {
					const conversation = this.#storage.get_conversation(selected_guid);
					const history = conversation[this.#storage.KEY_CONVERSATION_HISTORY];
					if (history && history[index]) {
						history[index].bookmark = !history[index].bookmark;
						conversation[this.#storage.KEY_CONVERSATION_HISTORY] = history;
						this.#storage.save_conversation(selected_guid, conversation);
						this.on_conversation_updated(false);
					}
				}
			};
		});

		this.div_response.querySelectorAll('.chain-toggle-btn').forEach((btn) => {
			btn.onclick = () => {
				const index = parseInt(btn.getAttribute('data-index'), 10);
				const selected_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
				if (selected_guid && !isNaN(index)) {
					const conversation = this.#storage.get_conversation(selected_guid);
					const history = conversation[this.#storage.KEY_CONVERSATION_HISTORY];
					if (history && history[index]) {
						history[index].chain = (history[index].chain === false) ? true : false;
						conversation[this.#storage.KEY_CONVERSATION_HISTORY] = history;
						this.#storage.save_conversation(selected_guid, conversation);
						this.on_conversation_updated(false);
					}
				}
			};
		});
	}

	/**
	 * Updates full conversation UI whenever active conversation data is modified.
	 * @param {boolean} [scroll_to_last=true] - Whether to scroll to bottom.
	 */
	on_conversation_updated(scroll_to_last = true) {
		const config = this.#storage.get_app_config();
		const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];

		this._sync_prompt_draft(selected_guid);

		if (!selected_guid) {
			this.state_i_open = false;
			this._reset_chat_view();
			this.apply_panels_layout();
			this.validate_conversation_index_button();
			this.#conversations_list?.apply_selected_index_class?.();
			this.#scratchpad_index?.highlight_active_index_item?.();
			return;
		}

		if (selected_guid === 'scratchpad') {
			this.state_i_open = true;
			if (window.innerWidth <= this.BREAKPOINT_TABLET) {
				this.state_v_open = false;
			}
			this.apply_panels_layout();
			this._setup_scratchpad_conversation_ui();
			this._reset_chat_view();
			this.#conversations_list?.apply_selected_index_class?.();
			this.#scratchpad_index?.highlight_active_index_item?.();
			this.validate_conversation_index_button();
			return;
		}

		setElementDisplay(this.div_chat_empty, 'none');
		setElementDisplay(this.div_chat_empty_header, 'none');
		(this.div_chat_ui_elements || []).forEach(el => setElementDisplay(el, ''));
		setElementDisplay(this.div_chat_title_bar, '');
		setElementDisplay(this.div_chat_prompt_inset, 'block');

		const conversation = this.#storage.get_conversation(selected_guid);
		const app_defaults = this.#storage.get_app_defaults();
		const type = conversation?.[this.#storage.KEY_CONVERSATION_TYPE] || this.#storage.CONVERSATION_TYPE_CHAT;

		if (type === this.#storage.CONVERSATION_TYPE_CHAT) {
			const count = this.get_conversation_item_count();
			if (count === 0 && this.state_i_open) {
				this.state_i_open = false;
				this.apply_panels_layout();
			}
			this._setup_chat_conversation_ui();
			if (this.btn_conversation_new) this.btn_conversation_new.textContent = 'New Chat';
		} else if (type === this.#storage.CONVERSATION_TYPE_SCRATCHPAD) {
			this._setup_scratchpad_conversation_ui();
			if (this.btn_conversation_new) this.btn_conversation_new.textContent = 'New';
		} else if (type === this.#storage.CONVERSATION_TYPE_NOTEBOOK) {
			this._setup_notebook_conversation_ui();
			if (this.btn_conversation_new) this.btn_conversation_new.textContent = 'New Notebook';
		}

		this.#conversations_list?.apply_selected_index_class?.();
		this.#scratchpad_index?.highlight_active_index_item?.();

		this._update_tab_visibility(this.state_active_tab, type, config[this.#storage.KEY_CONFIG_EXPERIMENTAL_FEATURES]);

		setElementDisplay(this.div_options_chips, 'block');

		const is_active_request = this.#api.is_request_active(selected_guid);
		if (is_active_request) {
			this.show_progress_ui(selected_guid);
		} else {
			this.hide_progress_ui();
			if (this.prompt) {
				this.validate_prompt();
				this.resize_prompt_textarea();
			}
		}

		const verbosity = conversation?.[this.#storage.KEY_CONVERSATION_VERBOSITY] || app_defaults?.[this.#storage.KEY_CONFIG_DEFAULT_VERBOSITY] || 'standard';
		const family = conversation?.[this.#storage.KEY_CONVERSATION_MODEL_FAMILY] || app_defaults?.[this.#storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY] || 'gemini';
		const model_version = conversation?.[this.#storage.KEY_CONVERSATION_MODEL_VERSION] || app_defaults?.[this.#storage.KEY_CONFIG_DEFAULT_MODEL_VERSION] || 'gemini-3.5-flash-lite';
		const short_model_name = formatModelShortName(family, model_version, Api.MODELS);

		if (this.span_option_model) {
			this.span_option_model.innerHTML = `<span style="opacity: 0.5">Model:</span> ${short_model_name}`;
		}
		if (this.span_option_verbosity) {
			this.span_option_verbosity.innerHTML = `<span style="opacity: 0.5">Verbosity:</span> ${verbosity}`;
		}

		const show_suggested = (conversation && conversation[this.#storage.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES] !== undefined)
			? conversation[this.#storage.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES]
			: (app_defaults?.[this.#storage.KEY_CONFIG_SHOW_SUGGESTED_QUERIES] ?? false);
		const show_related = (conversation && conversation[this.#storage.KEY_CONVERSATION_SHOW_RELATED_QUERIES] !== undefined)
			? conversation[this.#storage.KEY_CONVERSATION_SHOW_RELATED_QUERIES]
			: (app_defaults?.[this.#storage.KEY_CONFIG_SHOW_RELATED_QUERIES] ?? false);
		const enforce_topics = (conversation && conversation[this.#storage.KEY_CONVERSATION_ENFORCE_TOPICS] !== undefined)
			? conversation[this.#storage.KEY_CONVERSATION_ENFORCE_TOPICS]
			: (app_defaults?.[this.#storage.KEY_CONFIG_ENFORCE_TOPICS] ?? false);
		const auto_run = (conversation && conversation[this.#storage.KEY_CONVERSATION_AUTO_RUN_PROPOSED_QUERIES] !== undefined)
			? conversation[this.#storage.KEY_CONVERSATION_AUTO_RUN_PROPOSED_QUERIES]
			: (app_defaults?.[this.#storage.KEY_CONFIG_AUTO_RUN_PROPOSED_QUERIES] ?? false);

		if (this.span_option_suggested) {
			this.span_option_suggested.innerHTML = `<span style="opacity: 0.5">Suggested:</span> ${show_suggested ? 'ON' : 'OFF'}`;
		}
		if (this.span_option_related) {
			this.span_option_related.innerHTML = `<span style="opacity: 0.5">Related:</span> ${show_related ? 'ON' : 'OFF'}`;
		}

		const famObj = Api.MODELS?.[family];
		const modelObj = famObj?.models?.find(m => m.model === model_version);
		const can_ground = !!modelObj?.grounding && this._model_supports_web_search(model_version);

		if (this.span_option_search) {
			if (can_ground) {
				const google_search = conversation?.[this.#storage.KEY_CONVERSATION_GOOGLE_SEARCH] || false;
				this.span_option_search.innerHTML = `<span style="opacity: 0.5">Search:</span> ${google_search ? 'ON' : 'OFF'}`;
				this.span_option_search.style.display = '';
			} else {
				this.span_option_search.style.display = 'none';
			}
		}

		if (this.span_option_enforce_topics) {
			this.span_option_enforce_topics.innerHTML = `<span style="opacity: 0.5">Enforce Topics:</span> ${enforce_topics ? 'ON' : 'OFF'}`;
		}
		if (this.span_option_auto_run) {
			this.span_option_auto_run.innerHTML = `<span style="opacity: 0.5">Auto-Run:</span> ${auto_run ? 'ON' : 'OFF'}`;
		}

		this.render_conversation_header();
		if (this.div_subtitle && this.span_subtitle_toggle) {
			const is_subtitle_shown = this.div_subtitle.classList.contains('subtitle-shown');
			this.span_subtitle_toggle.classList.toggle('rotated', is_subtitle_shown);
		}

		if (this.state_active_tab === 'conversation') {
			void this.render_conversation_tab(scroll_to_last);
		} else if (this.state_active_tab === 'context') {
			this.render_context_tab();
		} else if (this.state_active_tab === 'memory' || this.state_active_tab === 'notebook-memory') {
			this.render_memory_tab();
		} else if (this.state_active_tab === 'document') {
			this.render_document_tab();
		} else if (this.state_active_tab === 'diction') {
			this.render_diction_tab();
		}

		this.validate_conversation_index_button();
	}

	//endregion

	//region Utilities / Helpers

	/**
	 * Synchronizes the prompt textarea content with the draft stored for the given conversation.
	 * @param {string} new_guid - The newly selected conversation GUID.
	 * @private
	 */
	_sync_prompt_draft(new_guid) {
		const current_key = new_guid || '';
		if (this.#active_conversation_guid !== current_key) {
			if (this.#active_conversation_guid !== null && this.prompt) {
				this.#prompt_drafts[this.#active_conversation_guid] = this.prompt.value;
			}
			this.#active_conversation_guid = current_key;
			if (this.prompt) {
				let draft = this.#prompt_drafts[current_key];
				if (draft === undefined && current_key) {
					const conv = this.#storage.get_conversation(current_key);
					const history = conv?.[this.#storage.KEY_CONVERSATION_HISTORY];
					const pendingItem = Array.isArray(history) ? history.find(item => item && item.pending) : null;
					if (pendingItem && pendingItem.query) {
						draft = pendingItem.query;
						this.#prompt_drafts[current_key] = draft;
					}
				}
				this.prompt.value = draft || '';
				this.resize_prompt_textarea();
				this.validate_prompt();
			}
		}
	}

	/**
	 * Returns whether a model supports web search. Gemini 2.5 models cannot
	 * combine built-in search grounding with function calling in one request,
	 * so web search is not offered for them.
	 * @param {string|null} modelId - Model API identifier.
	 * @returns {boolean}
	 */
	_model_supports_web_search(modelId) {
		return !(typeof modelId === 'string' && modelId.startsWith('gemini-2.5'));
	}

	/**
	 * Returns the selected instant answer model's API identifier.
	 * @returns {string}
	 */
	_instant_answer_model_id() {
		const utilityKey = this.select_instant_answer_model?.value || 'gemini-3.1-fl';
		return Api.UTILITY_MODELS?.[utilityKey]?.model || utilityKey;
	}

	/**
	 * Shows the Instant Answer modal overlay, populating models and cost estimation.
	 */
	show_instant_answer() {
		const config = this.#storage.get_app_config();
		const appUtilityModel = this.select_utility_model?.value || (config && config[this.#storage.KEY_CONFIG_UTILITY_MODEL]) || 'gemini-3.1-fl';
		const instantAnswerModel = config[this.#storage.KEY_CONFIG_INSTANT_ANSWER_MODEL] || appUtilityModel;
		if (this.select_instant_answer_model) {
			populateUtilityModelDropdown(this.select_instant_answer_model, Api.UTILITY_MODELS, instantAnswerModel);
			this.select_instant_answer_model.value = instantAnswerModel;
		}
		this.update_instant_answer_search_button();
		this.update_instant_answer_model_cost_display();
		this.resize_instant_answer_textarea();
		showOverlay(this.div_instant_answer_overlay);
		this.resize_instant_answer_textarea();
		setTimeout(() => {
			if (this.instant_answer_query) {
				this.resize_instant_answer_textarea();
				this.instant_answer_query.focus();
			}
		}, 50);
	}

	/**
	 * Hides the Instant Answer modal overlay and clears both the query and response.
	 */
	hide_instant_answer() {
		if (this.instant_answer_query) {
			this.instant_answer_query.value = '';
			this.instant_answer_query.style.height = '';
		}
		if (this.div_instant_answer_response) {
			this.div_instant_answer_response.innerHTML = '';
		}
		if (this.div_instant_answer_cost) {
			this.div_instant_answer_cost.innerHTML = '';
			this.div_instant_answer_cost.style.display = 'none';
		}
		if (this.div_instant_answer_response_container) {
			this.div_instant_answer_response_container.style.display = 'none';
		}
		if (this.div_instant_answer_status) {
			this.div_instant_answer_status.style.display = 'none';
			this.div_instant_answer_status.textContent = '';
		}
		if (this.btn_instant_answer_send) {
			this.btn_instant_answer_send.disabled = false;
			this.btn_instant_answer_send.textContent = 'Send';
		}
		if (this.div_instant_answer_overlay) {
			hideOverlay(this.div_instant_answer_overlay);
		}
	}

	/**
	 * Adjusts the height of the instant answer query textarea to match its content.
	 */
	resize_instant_answer_textarea() {
		if (!this.instant_answer_query) return;
		this.instant_answer_query.style.height = 'auto';
		if (this.instant_answer_query.scrollHeight > 0) {
			this.instant_answer_query.style.height = this.instant_answer_query.scrollHeight + 'px';
		}
	}

	/**
	 * Toggles the Instant Answer web search setting and persists it.
	 */
	toggle_instant_answer_search() {
		const newState = !this.get_instant_answer_search_state();
		this.#storage.update_app_config(this.#storage.KEY_CONFIG_INSTANT_ANSWER_SEARCH, newState);
		this.apply_instant_answer_search_state(newState);
		this.update_instant_answer_model_cost_display();
	}

	/**
	 * Returns whether web search is enabled for Instant Answer.
	 * Always false for models that do not support web search.
	 * @returns {boolean}
	 */
	get_instant_answer_search_state() {
		if (!this._model_supports_web_search(this._instant_answer_model_id())) {
			return false;
		}
		return this.btn_instant_answer_search?.dataset.search === 'on';
	}

	/**
	 * Shows or hides the search toggle based on model support and applies the persisted state.
	 */
	update_instant_answer_search_button() {
		if (!this.btn_instant_answer_search) return;
		const available = this._model_supports_web_search(this._instant_answer_model_id());
		this.btn_instant_answer_search.style.display = available ? '' : 'none';
		const enabled = available && (this.#storage.get_app_config()[this.#storage.KEY_CONFIG_INSTANT_ANSWER_SEARCH] || false);
		this.apply_instant_answer_search_state(enabled);
	}

	/**
	 * Applies the search on/off state to the toggle button label.
	 * @param {boolean} enabled - Whether search is enabled.
	 */
	apply_instant_answer_search_state(enabled) {
		if (this.btn_instant_answer_search) {
			this.btn_instant_answer_search.dataset.search = enabled ? 'on' : 'off';
			const stateColor = enabled ? 'var(--TX-status-success)' : 'var(--FG-muted)';
			this.btn_instant_answer_search.innerHTML = `Search: <strong style="color: ${stateColor};">${enabled ? 'ON' : 'OFF'}</strong>`;
		}
	}

	/**
	 * Updates the estimated cost display for the selected instant answer model.
	 */
	update_instant_answer_model_cost_display() {
		if (!this.div_instant_answer_model_cost) return;
		const utilityKey = this.select_instant_answer_model?.value || 'gemini-3.1-fl';
		const utilityConfig = Api.UTILITY_MODELS?.[utilityKey];
		const modelId = utilityConfig?.model || utilityKey;
		const thinking = utilityConfig?.thinking || null;
		const costInfo = calculateUtilityCost(modelId, thinking);
		let total = costInfo.total * 8;
		if (this.get_instant_answer_search_state()) {
			total += 6 * getSearchUseCost(modelId);
		}
		const formattedCost = formatEstimatedCost(total);
		this.div_instant_answer_model_cost.innerHTML = `<span class="span-utility-cost-label">Estimated Cost:</span><span class="span-utility-cost-val">${formattedCost}</span>`;
	}

	/**
	 * Handles keydown events in the instant answer query textarea.
	 * @param {KeyboardEvent} e - The keyboard event.
	 */
	handle_instant_answer_keydown(e) {
		if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
			e.preventDefault();
			if (!this.btn_instant_answer_send || !this.btn_instant_answer_send.disabled) {
				this.send_instant_answer();
			}
		}
	}

	/**
	 * Sends the instant answer query to the backend ApiUtility service with live thinking stream support.
	 * @returns {Promise<void>}
	 */
	async send_instant_answer() {
		if (!this.instant_answer_query) return;
		const query = this.instant_answer_query.value.trim();
		if (!query) {
			if (this.div_instant_answer_status) {
				this.div_instant_answer_status.textContent = 'Please enter a query.';
				this.div_instant_answer_status.className = 'status-msg status-error';
				this.div_instant_answer_status.style.display = 'block';
			}
			return;
		}

		const selectedModel = this.select_instant_answer_model ? this.select_instant_answer_model.value : 'gemini-3.1-fl';

		// Set UI loading state
		if (this.btn_instant_answer_send) {
			this.btn_instant_answer_send.disabled = true;
			this.btn_instant_answer_send.textContent = 'Sending...';
		}
		if (this.div_instant_answer_status) {
			this.div_instant_answer_status.style.display = 'none';
			this.div_instant_answer_status.textContent = '';
		}

		// Reset previous response while loading new one
		const streamedThoughts = [];
		if (this.div_instant_answer_response) {
			this.div_instant_answer_response.innerHTML = '<em>Thinking...</em>';
		}
		if (this.div_instant_answer_cost) {
			this.div_instant_answer_cost.innerHTML = '';
			this.div_instant_answer_cost.style.display = 'none';
		}
		if (this.div_instant_answer_response_container) {
			this.div_instant_answer_response_container.style.display = 'block';
		}

		let thoughtsContentDiv = null;

		const onThinking = (thoughtText) => {
			if (!thoughtText) return;
			streamedThoughts.push(thoughtText);
			if (this.div_instant_answer_response) {
				if (!thoughtsContentDiv || !this.div_instant_answer_response.contains(thoughtsContentDiv)) {
					this.div_instant_answer_response.innerHTML = `<details open class="instant-answer-thoughts"><summary>Thinking...</summary><div class="instant-answer-thoughts-content streaming"></div></details>`;
					thoughtsContentDiv = this.div_instant_answer_response.querySelector('.instant-answer-thoughts-content');
				}
				if (thoughtsContentDiv) {
					const lines = String(thoughtText).split("\n").filter(l => l.length > 0);
					const htmlChunks = lines.map(l => formatThinkingLineHtml(l)).join('');
					thoughtsContentDiv.insertAdjacentHTML('beforeend', htmlChunks);
					thoughtsContentDiv.scrollTop = thoughtsContentDiv.scrollHeight;
				}
			}
		};

		try {
			const response = await this.#api.evaluate_utility(query, 'instant_answer', selectedModel, onThinking, null, {
				google_search: this.get_instant_answer_search_state()
			});
			const finalThoughts = (Array.isArray(response?.thinking) && response.thinking.length > 0) ? response.thinking : streamedThoughts;
			if (response && response.error) {
				if (this.div_instant_answer_status) {
					this.div_instant_answer_status.textContent = response.error;
					this.div_instant_answer_status.className = 'status-msg status-error';
					this.div_instant_answer_status.style.display = 'block';
				}
				if (this.div_instant_answer_response) {
					let html = '';
					if (finalThoughts.length > 0) {
						const thoughtsFormatted = finalThoughts.map(t => formatThinkingLineHtml(t)).join('');
						html += `<details class="instant-answer-thoughts"><summary>Thought Process (${finalThoughts.length} lines)</summary><div class="instant-answer-thoughts-content">${thoughtsFormatted}</div></details>`;
					}
					html += `<span style="color: var(--tx-status-error, #ff6b6b);">${escapeHtml(response.error)}</span>`;
					this.div_instant_answer_response.innerHTML = html;
				}
			} else {
				const explanation = response?.explanation || (typeof response === 'string' ? response : 'No answer returned.');
				let html = '';
				if (finalThoughts.length > 0) {
					const thoughtsFormatted = finalThoughts.map(t => formatThinkingLineHtml(t)).join('');
					html += `<details class="instant-answer-thoughts"><summary>Thought Process (${finalThoughts.length} lines)</summary><div class="instant-answer-thoughts-content">${thoughtsFormatted}</div></details>`;
				}
				html += `<div class="instant-answer-body">${explanation}</div>`;
				if (this.get_instant_answer_search_state()) {
					html += formatAnnotationsBlock(response?.annotations);
				}
				if (this.div_instant_answer_response) {
					this.div_instant_answer_response.innerHTML = html;
					highlightCodeBlocks(this.div_instant_answer_response);
					await this._render_mermaid_in_container(this.div_instant_answer_response);
				}
				if (this.div_instant_answer_cost) {
					if (response && typeof response === 'object') {
						const costHtml = formatCostSummary(response);
						this.div_instant_answer_cost.innerHTML = costHtml;
						this.div_instant_answer_cost.style.display = costHtml ? 'block' : 'none';
					} else {
						this.div_instant_answer_cost.innerHTML = '';
						this.div_instant_answer_cost.style.display = 'none';
					}
				}
			}
		} catch (error) {
			console.error('Instant answer request failed:', error);
			const errMsg = error?.message || error?.error || 'Failed to fetch instant answer.';
			if (this.div_instant_answer_status) {
				this.div_instant_answer_status.textContent = errMsg;
				this.div_instant_answer_status.className = 'status-msg status-error';
				this.div_instant_answer_status.style.display = 'block';
			}
			if (this.div_instant_answer_response) {
				let html = '';
				if (streamedThoughts.length > 0) {
					const thoughtsFormatted = streamedThoughts.map(t => formatThinkingLineHtml(t)).join('');
					html += `<details class="instant-answer-thoughts"><summary>Thought Process (${streamedThoughts.length} lines)</summary><div class="instant-answer-thoughts-content">${thoughtsFormatted}</div></details>`;
				}
				html += `<span style="color: var(--tx-status-error, #ff6b6b);">${escapeHtml(errMsg)}</span>`;
				this.div_instant_answer_response.innerHTML = html;
			}
			if (this.div_instant_answer_cost) {
				this.div_instant_answer_cost.innerHTML = '';
				this.div_instant_answer_cost.style.display = 'none';
			}
		} finally {
			if (this.btn_instant_answer_send) {
				this.btn_instant_answer_send.disabled = false;
				this.btn_instant_answer_send.textContent = 'Send';
			}
		}
	}

	/**
	 * Shows the application options form overlay.
	 */
	show_app_options() {
		showOverlay(this.div_app_options_overlay);
		requestAnimationFrame(() => {
			if (Users.instance) {
				Users.instance.loadProfile();
				if (Users.isAdmin) {
					Users.instance.loadAdminUsers();
				}
			}
			setElementDisplay(this.form_admin_options, Users.isAdmin ? 'block' : 'none');
			this.apply_app_options();
			this.apply_app_defaults();
			if (!this.#app_version_data) {
				void this.load_app_version();
			} else {
				this.update_app_version_settings(this.#app_version_data);
			}
			const config = this.#storage.get_app_config();
			if (this.checkbox_experimental_features) {
				this.checkbox_experimental_features.checked = config[this.#storage.KEY_CONFIG_EXPERIMENTAL_FEATURES] || false;
			}
			if (this.checkbox_background_keep_alive) {
				this.checkbox_background_keep_alive.checked = config[this.#storage.KEY_CONFIG_BACKGROUND_KEEP_ALIVE] || false;
			}
			if (this.checkbox_experimental_mistral) {
				this.checkbox_experimental_mistral.checked = config[this.#storage.KEY_CONFIG_ENABLE_MISTRAL] || false;
			}
			if (this.checkbox_experimental_organizer) {
				this.checkbox_experimental_organizer.checked = config[this.#storage.KEY_CONFIG_ORGANIZER] || false;
			}
			this.update_experimental_options_visibility();
		});
	}

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

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

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

	/**
	 * Hides both options overlays.
	 */
	hide_options_overlay() {
		this.hide_app_options();
		this.hide_conversation_options();
	}

	/**
	 * Handles clicks on options overlays to dismiss modal dialogs.
	 * @param {Event} e - The click event.
	 */
	handle_options_overlay_click(e) {
		if (e.target === this.div_app_options_overlay) {
			this.hide_app_options();
		} else if (e.target === this.div_conversation_options_overlay) {
			this.hide_conversation_options();
		}
	}

	/**
	 * Handles clicks on the shared backdrop to dismiss the topmost open overlay.
	 * @param {Event} e - The click event.
	 */
	handle_backdrop_click(e) {
		if (e.target !== this.div_dialog_backdrop) return;
		if (this.div_version_overlay && this.div_version_overlay.classList.contains('active')) {
			this.hide_version_overlay();
		} else if (this.div_instant_answer_overlay && this.div_instant_answer_overlay.classList.contains('active')) {
			this.hide_instant_answer();
		} else if (this.div_conversation_options_overlay && this.div_conversation_options_overlay.classList.contains('active')) {
			this.hide_conversation_options();
		} else if (this.div_app_options_overlay && this.div_app_options_overlay.classList.contains('active')) {
			this.hide_app_options();
		}
	}

	/**
	 * Retrieves the conversation object for the currently selected GUID from storage.
	 * @returns {Object|null} The conversation object or null if none is selected.
	 */
	get_selected_conversation() {
		return this.#storage.get_selected_conversation();
	}

	/**
	 * Checks if a response or history item contains valid content to be rendered or sent.
	 * @param {Object} item - History turn or response payload.
	 * @returns {boolean}
	 * @private
	 */
	_has_valid_content(item) {
		if (!item) return false;
		if (typeof item.content === 'string' && item.content.length > 0) return true;
		if (Array.isArray(item.content) && item.content.length > 0) return true;
		if (typeof item.content === 'object' && item.content !== null && Object.keys(item.content).length > 0) return true;
		if (Array.isArray(item.thinking) && item.thinking.length > 0) return true;
		return typeof item.thinking === 'string' && item.thinking.trim().length > 0;
	}

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

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

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

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

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

		const thinking_val = is_app
			? (defaults?.[this.#storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL] !== undefined ? defaults[this.#storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL] : 'MINIMAL')
			: (conversation?.[this.#storage.KEY_CONVERSATION_THINKING_LEVEL] !== undefined ? conversation[this.#storage.KEY_CONVERSATION_THINKING_LEVEL] : (app_defaults?.[this.#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.#storage.KEY_CONFIG_DEFAULT_MODEL] !== undefined ? defaults[this.#storage.KEY_CONFIG_DEFAULT_MODEL] : matchedPreset)
			: (conversation?.[this.#storage.KEY_CONVERSATION_MODEL] !== undefined ? conversation[this.#storage.KEY_CONVERSATION_MODEL] : (conversation ? matchedPreset : (app_defaults?.[this.#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.#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.#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.#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.#storage.KEY_CONFIG_AUTO_RUN_PROPOSED_QUERIES] : false));
		const web_search_default_val = is_app
			? (defaults ? defaults[this.#storage.KEY_CONFIG_ENABLE_WEB_SEARCH] : false)
			: false;

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

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

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

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

		populatePresetDropdown(preset_select, this._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.div_default_model_description : this.div_conversation_model_description;
		const costEl = is_app ? this.div_default_model_costs : this.div_conversation_model_costs;
		updateModelDetailsDisplay(descEl, costEl, selected_family, version_val, thinking_val, Api.MODELS, verbosity_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.checkbox_default_play_chime) this.checkbox_default_play_chime.checked = !!play_chime_val;
		if (is_app && this.checkbox_default_web_search) this.checkbox_default_web_search.checked = !!web_search_default_val;
		if (is_app && this.div_default_web_search_container) {
			this.div_default_web_search_container.style.display = this._model_supports_web_search(modelObj?.model) ? 'block' : 'none';
		}

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

			const can_ground = !!modelObj?.grounding && this._model_supports_web_search(modelObj?.model);
			if (this.div_conversation_google_search_container) {
				this.div_conversation_google_search_container.style.display = can_ground ? 'block' : 'none';
				const search_label = this.div_conversation_google_search_container.querySelector('label');
				if (search_label) {
					search_label.textContent = 'Enable Web Search';
				}
			}
		}
	}

	/**
	 * 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.
	 * @private
	 */
	async _on_preset_change(type, e) {
		const is_app = type === 'app';
		const newPresetKey = e.target.value;

		if (!newPresetKey) {
			if (is_app) {
				this.#storage.update_app_defaults(this.#storage.KEY_CONFIG_DEFAULT_MODEL, '');
			} else {
				const selected_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
				if (selected_guid) {
					this.#storage.update_conversation_field(selected_guid, this.#storage.KEY_CONVERSATION_MODEL, '');
				}
			}
			this._apply_options(type);
			this.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.#storage.update_app_defaults(this.#storage.KEY_CONFIG_DEFAULT_MODEL, newPresetKey);
				this.#storage.update_app_defaults(this.#storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY, pFamily);
				this.#storage.update_app_defaults(this.#storage.KEY_CONFIG_DEFAULT_MODEL_VERSION, pModel);
				this.#storage.update_app_defaults(this.#storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL, pThinking);
			} else {
				const selected_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
				if (selected_guid) {
					this.#storage.update_conversation_field(selected_guid, this.#storage.KEY_CONVERSATION_MODEL, newPresetKey);
					this.#storage.update_conversation_field(selected_guid, this.#storage.KEY_CONVERSATION_MODEL_FAMILY, pFamily);
					this.#storage.update_conversation_field(selected_guid, this.#storage.KEY_CONVERSATION_MODEL_VERSION, pModel);
					this.#storage.update_conversation_field(selected_guid, this.#storage.KEY_CONVERSATION_THINKING_LEVEL, pThinking);
				}
			}
			this._apply_options(type);
			this.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.
	 * @private
	 */
	_on_family_change(type, e) {
		const is_app = type === 'app';
		const newFamily = e.target.value;
		const modelsList = Api.MODELS?.[newFamily]?.models || [];
		const firstModel = modelsList[0]?.model || '';
		const firstThinking = modelsList[0]?.thinking_modes?.[0] ?? null;

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

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

		this._apply_options(type);
		this.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.
	 * @private
	 */
	_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.#storage.get_app_defaults();
			family = defaults[this.#storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY] || 'gemini';
			currentThinking = defaults[this.#storage.KEY_CONFIG_DEFAULT_THINKING_LEVEL];
		} else {
			const conversation = this.get_selected_conversation();
			const app_defaults = this.#storage.get_app_defaults();
			family = conversation?.[this.#storage.KEY_CONVERSATION_MODEL_FAMILY] || app_defaults?.[this.#storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY] || 'gemini';
			currentThinking = conversation?.[this.#storage.KEY_CONVERSATION_THINKING_LEVEL] !== undefined
				? conversation[this.#storage.KEY_CONVERSATION_THINKING_LEVEL]
				: app_defaults?.[this.#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 = thinkingModes[0] ?? null;
		}

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

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

		this._apply_options(type);
		this.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.
	 * @private
	 */
	_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.#storage.get_app_defaults();
			family = defaults[this.#storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY] || 'gemini';
			modelVersion = defaults[this.#storage.KEY_CONFIG_DEFAULT_MODEL_VERSION] || 'gemini-3.5-flash-lite';
		} else {
			const conversation = this.get_selected_conversation();
			const app_defaults = this.#storage.get_app_defaults();
			family = conversation?.[this.#storage.KEY_CONVERSATION_MODEL_FAMILY] || app_defaults?.[this.#storage.KEY_CONFIG_DEFAULT_MODEL_FAMILY] || 'gemini';
			modelVersion = conversation?.[this.#storage.KEY_CONVERSATION_MODEL_VERSION] || app_defaults?.[this.#storage.KEY_CONFIG_DEFAULT_MODEL_VERSION] || 'gemini-3.5-flash-lite';
		}

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

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

		this._apply_options(type);
		this.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.
	 * @private
	 */
	_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.#storage.KEY_CONFIG_DEFAULT_VERBOSITY,
			'id-select-conversation-verbosity': this.#storage.KEY_CONVERSATION_VERBOSITY,
			'id-checkbox-default-show-suggestions': this.#storage.KEY_CONFIG_SHOW_SUGGESTED_QUERIES,
			'id-checkbox-default-show-related': this.#storage.KEY_CONFIG_SHOW_RELATED_QUERIES,
			'id-checkbox-default-enforce-topics': this.#storage.KEY_CONFIG_ENFORCE_TOPICS,
			'id-checkbox-default-auto-send-prompts': this.#storage.KEY_CONFIG_AUTO_RUN_PROPOSED_QUERIES,
			'id-checkbox-default-play-chime': this.#storage.KEY_CONFIG_PLAY_CHIME,
			'id-checkbox-default-web-search': this.#storage.KEY_CONFIG_ENABLE_WEB_SEARCH,
			'id-checkbox-conversation-show-suggestions': this.#storage.KEY_CONVERSATION_SHOW_SUGGESTED_QUERIES,
			'id-checkbox-conversation-show-related': this.#storage.KEY_CONVERSATION_SHOW_RELATED_QUERIES,
			'id-checkbox-conversation-enforce-topics': this.#storage.KEY_CONVERSATION_ENFORCE_TOPICS,
			'id-checkbox-conversation-auto-send-prompts': this.#storage.KEY_CONVERSATION_AUTO_RUN_PROPOSED_QUERIES,
			'id-checkbox-conversation-google-search': this.#storage.KEY_CONVERSATION_GOOGLE_SEARCH
		};

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

		if (is_app) {
			this.#storage.update_app_defaults(key, value);
		} else {
			const selected_guid = this.#storage.get_app_config()[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
			if (selected_guid) {
				this.#storage.update_conversation_field(selected_guid, key, value);
			}
		}

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

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

	/**
	 * Shows a specific options overlay.
	 * @param {('app'|'conversation')} form_type - The type of form to show.
	 * @private
	 */
	_show_options(form_type) {
		if (form_type === 'app') {
			this.show_app_options();
		} else {
			this.show_conversation_options();
		}
	}

	/**
	 * Resets the chat view to its empty state when no conversation is selected.
	 * @private
	 */
	_reset_chat_view() {
		setElementDisplay(this.div_chat_empty, 'grid');
		setElementDisplay(this.div_chat_empty_header, 'block');
		(this.div_chat_ui_elements || []).forEach(el => setElementDisplay(el, 'none'));
		setElementDisplay(this.div_options_chips, 'none');

		if (this.div_title) this.div_title.innerHTML = '';
		if (this.div_subtitle) {
			this.div_subtitle.innerHTML = '';
			this.div_subtitle.style.maxHeight = null;
		}
		if (this.span_subtitle_toggle) {
			this.span_subtitle_toggle.classList.remove('rotated');
		}

		setElementDisplay(this.div_chat_prompt_container, 'none');
		setElementDisplay(this.div_chat_title_bar, 'none');
		this._update_tab_visibility(null, null, false);
		setElementDisplay(this.div_chat_prompt_inset, 'none');

		setElementDisplay(this.btn_empty_new_scratchpad, 'block');
		setElementDisplay(this.btn_show_scratchpads_new, 'block');
		setElementDisplay(this.btn_empty_new_chat, 'block');
		setElementDisplay(this.btn_show_conversations_new, 'block');
		const config = this.#storage?.get_app_config();
		const experimental_features_enabled = config?.[this.#storage.KEY_CONFIG_EXPERIMENTAL_FEATURES] || false;
		setElementDisplay(this.btn_empty_new_notebook, experimental_features_enabled ? 'block' : 'none');
		const notebookRow = getEl('id-div-empty-notebook-row');
		if (notebookRow) {
			notebookRow.style.display = experimental_features_enabled ? 'flex' : 'none';
		}
	}

	/**
	 * Checks whether the active conversation is a scratchpad.
	 * @returns {boolean} True if scratchpad is active.
	 */
	is_scratchpad_active() {
		const config = this.#storage.get_app_config();
		const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
		if (selected_guid === 'scratchpad') {
			return true;
		}
		const conversation = this.#storage.get_conversation(selected_guid);
		return conversation && conversation[this.#storage.KEY_CONVERSATION_TYPE] === this.#storage.CONVERSATION_TYPE_SCRATCHPAD;
	}

	/**
	 * Encapsulates all logic for showing/hiding tab buttons and scroll containers declaratively.
	 * @param {string|null} activeTabName - The name of the currently active tab.
	 * @param {string|null} conversationType - The type of the current conversation.
	 * @param {boolean} experimentalFeaturesEnabled - Whether experimental features are enabled.
	 * @private
	 */
	_update_tab_visibility(activeTabName, conversationType, experimentalFeaturesEnabled) {
		const tabs = {
			conversation: { btn: this.btn_tab_conversation, scroll: this.div_chat_container_scroll, type: 'chat' },
			context: { btn: this.btn_tab_context, scroll: this.div_chat_context_scroll, type: 'chat' },
			memory: { btn: this.btn_tab_memory, scroll: this.div_chat_memory_scroll, type: 'chat', experimental: true },
			document: { btn: this.btn_tab_document, scroll: this.div_chat_document_scroll, type: 'notebook' },
			diction: { btn: this.btn_tab_diction, scroll: this.div_chat_diction_scroll, type: 'notebook' },
			'notebook-memory': { btn: this.btn_tab_notebook_memory, scroll: this.div_notebook_memory_scroll, type: 'notebook', experimental: true }
		};

		// Hide all tab buttons and scroll containers initially
		for (const key in tabs) {
			if (tabs[key].btn) {
				setElementDisplay(tabs[key].btn, 'none');
				tabs[key].btn.classList.remove('active');
			}
			if (tabs[key].scroll) {
				setElementDisplay(tabs[key].scroll, 'none');
			}
		}

		setElementDisplay(this.div_chat_tab_bar, 'none');
		setElementDisplay(this.div_notebook_tab_bar, 'none');
		setElementDisplay(this.div_chat_prompt_container, 'none');

		if (!activeTabName || !conversationType) {
			return;
		}

		// Show relevant tab bars and buttons based on conversation type
		if (conversationType === this.#storage.CONVERSATION_TYPE_CHAT || conversationType === this.#storage.CONVERSATION_TYPE_SCRATCHPAD) {
			setElementDisplay(this.div_chat_tab_bar, '');
			setElementDisplay(tabs.conversation.btn, 'inline-block');
			setElementDisplay(tabs.context.btn, 'inline-block');
			if (experimentalFeaturesEnabled) {
				setElementDisplay(tabs.memory.btn, 'inline-block');
			}
		} else if (conversationType === this.#storage.CONVERSATION_TYPE_NOTEBOOK) {
			setElementDisplay(this.div_notebook_tab_bar, '');
			setElementDisplay(tabs.document.btn, 'inline-block');
			setElementDisplay(tabs.diction.btn, 'inline-block');
			if (experimentalFeaturesEnabled) {
				setElementDisplay(tabs['notebook-memory'].btn, 'inline-block');
			}
		}

		// Set active tab and show its content
		const currentTab = tabs[activeTabName];
		if (currentTab) {
			const is_visible_by_type = currentTab.type === ((conversationType === this.#storage.CONVERSATION_TYPE_CHAT || conversationType === this.#storage.CONVERSATION_TYPE_SCRATCHPAD) ? 'chat' : 'notebook');
			const is_visible_by_experimental = !currentTab.experimental || experimentalFeaturesEnabled;

			if (is_visible_by_type && is_visible_by_experimental) {
				if (currentTab.btn) {
					currentTab.btn.classList.add('active');
				}
				if (currentTab.scroll) {
					setElementDisplay(currentTab.scroll, 'grid');
				}
			} else {
				if (conversationType === this.#storage.CONVERSATION_TYPE_CHAT || conversationType === this.#storage.CONVERSATION_TYPE_SCRATCHPAD) {
					this.state_active_tab = 'conversation';
					if (tabs.conversation.btn) tabs.conversation.btn.classList.add('active');
					if (tabs.conversation.scroll) setElementDisplay(tabs.conversation.scroll, 'grid');
				} else if (conversationType === this.#storage.CONVERSATION_TYPE_NOTEBOOK) {
					this.state_active_tab = 'document';
					if (tabs.document.btn) tabs.document.btn.classList.add('active');
					if (tabs.document.scroll) setElementDisplay(tabs.document.scroll, 'grid');
				}
			}
		} else {
			if (conversationType === this.#storage.CONVERSATION_TYPE_CHAT || conversationType === this.#storage.CONVERSATION_TYPE_SCRATCHPAD) {
				this.state_active_tab = 'conversation';
				if (tabs.conversation.btn) tabs.conversation.btn.classList.add('active');
				if (tabs.conversation.scroll) setElementDisplay(tabs.conversation.scroll, 'grid');
			} else if (conversationType === this.#storage.CONVERSATION_TYPE_NOTEBOOK) {
				this.state_active_tab = 'document';
				if (tabs.document.btn) tabs.document.btn.classList.add('active');
				if (tabs.document.scroll) setElementDisplay(tabs.document.scroll, 'grid');
			}
		}

		const tabsWithPrompt = ['conversation', 'document'];
		if (this.div_chat_prompt_container) {
			setElementDisplay(this.div_chat_prompt_container, tabsWithPrompt.includes(this.state_active_tab) ? 'grid' : 'none');
		}
	}

	/**
	 * Sets up UI panels and tab bars for a standard chat conversation.
	 * @private
	 */
	_setup_chat_conversation_ui() {
		setElementDisplay(this.div_chat_tab_bar, '');
		setElementDisplay(this.div_notebook_tab_bar, 'none');
		setElementDisplay(this.div_index_list, '');
		setElementDisplay(this.div_structure_center_index_options, 'grid');
		setElementDisplay(this.div_structure_center_notebook_options, 'none');
		this.#conversation_index.on_conversation_index_updated();
		if (this.div_chat_memory_scroll) {
			this.div_chat_memory_scroll.classList.add('chat-memory-style');
		}
	}

	/**
	 * Sets up UI panels and tab bars for a scratchpad conversation.
	 * @private
	 */
	_setup_scratchpad_conversation_ui() {
		setElementDisplay(this.div_chat_tab_bar, '');
		setElementDisplay(this.div_notebook_tab_bar, 'none');
		setElementDisplay(this.div_index_list, '');
		setElementDisplay(this.div_structure_center_index_options, 'grid');
		setElementDisplay(this.div_structure_center_notebook_options, 'none');
		this.#scratchpad_index.on_conversation_index_updated();
		if (this.div_chat_memory_scroll) {
			this.div_chat_memory_scroll.classList.add('chat-memory-style');
		}
	}

	/**
	 * Sets up UI panels and tab bars for a notebook conversation.
	 * @private
	 */
	_setup_notebook_conversation_ui() {
		setElementDisplay(this.div_chat_tab_bar, 'none');
		setElementDisplay(this.div_notebook_tab_bar, '');
		setElementDisplay(this.div_index_list, 'none');
		setElementDisplay(this.div_structure_center_index_options, 'none');
		setElementDisplay(this.div_structure_center_notebook_options, 'grid');
		this.#notebook_index.render(this.div_index_list);
		if (this.div_chat_memory_scroll) {
			this.div_chat_memory_scroll.classList.add('notebook-memory-style');
		}
	}
}

export default App;
🌐
app.js ×
Type: Web, text/plain
150.45 Kilobytes
Last Modified 2026-09-23 10:29:58
⬇ Download File