2 directories, 29 files

tinai

Home / testing / ai / tinai
import { createElementFromHTML } from './util/dom-utils.js';
import { SelectionManager } from './util/selection-manager.js';
import { customConfirm } from './util/confirm-dialog.js';

/**
 * class ScratchpadIndex
 * Manages the secondary navigation index for Scratchpad conversations.
 * Handles the display, selection, bookmarking, and deletion of scratchpad conversations
 * within the index panel.
 */
class ScratchpadIndex {

	#storage;
	#app_callbacks;
	#selection;

	div_index_list;
	btn_conversation_index_select;
	btn_conversation_index_favorite;
	btn_conversation_index_delete;

	BREAKPOINT_MOBILE;

	/**
	 * Initializes the ScratchpadIndex instance with storage, callbacks, element references, and breakpoints.
	 * @param {Storage} storage_instance - Storage manager instance.
	 * @param {object} app_callbacks - Application callback methods.
	 * @param {object} elements - DOM element references.
	 * @param {object} breakpoints - Responsive breakpoint constants.
	 */
	constructor(storage_instance, app_callbacks, elements, breakpoints) {
		this.#storage = storage_instance;
		this.#app_callbacks = app_callbacks;

		this.div_index_list = elements.div_index_list;
		this.btn_conversation_index_select = elements.btn_conversation_index_select;
		this.btn_conversation_index_favorite = elements.btn_conversation_index_favorite;
		this.btn_conversation_index_delete = elements.btn_conversation_index_delete;

		this.BREAKPOINT_MOBILE = breakpoints.BREAKPOINT_MOBILE;

		this.#selection = new SelectionManager(() => this._update_selection_ui());
	}

	//region Scratchpad Selection and Actions

	/**
	 * Updates the selection controls UI state.
	 * @private
	 */
	_update_selection_ui() {
		this.#selection.updateControls(
			this.btn_conversation_index_select,
			[this.btn_conversation_index_favorite, this.btn_conversation_index_delete],
			'Select'
		);
	}

	/**
	 * Toggles multi-selection mode for scratchpad conversations in the index.
	 */
	toggle_response_selection_mode() {
		this.#selection.toggleMode();
		this._update_selection_ui();
		this.on_conversation_index_updated();
	}

	/**
	 * Toggles the bookmarked status of all scratchpad conversations currently selected.
	 */
	bookmark_selected_responses() {
		const selectedGuids = this.#selection.selectedItems;
		if (selectedGuids.length === 0) return;

		selectedGuids.forEach(guid => {
			const conversation = this.#storage.get_conversation(guid);
			if (conversation) {
				const isBookmarked = conversation.BOOKMARKED || false;
				conversation.BOOKMARKED = !isBookmarked;
				this.#storage.save_conversation(guid, conversation);
			}
		});

		this.#selection.clear();
		this._update_selection_ui();
		this.on_conversation_index_updated();
		this.#app_callbacks.on_conversation_updated_main_panel(false);
	}

	/**
	 * Deletes all scratchpad conversations currently selected from the index after user confirmation.
	 */
	async delete_selected_responses() {
		const selectedGuids = this.#selection.selectedItems;
		const count = selectedGuids.length;
		if (count === 0) return;

		const message = count === 1
			? 'Are you sure you want to delete this scratchpad?'
			: `Are you sure you want to delete ${count} selected scratchpads?`;

		if (await customConfirm('Delete Scratchpads', message)) {
			const config = this.#storage.get_app_config();
			const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];

			const toDelete = [...selectedGuids];
			toDelete.forEach(guid => {
				this.#storage.index_delete(guid);
			});

			this.#selection.clear();
			this._update_selection_ui();

			const remainingCount = this.get_scratchpad_conversations().length;
			if (remainingCount > 0) {
				if (toDelete.includes(selected_guid)) {
					this.#app_callbacks.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, 'scratchpad');
				}
				this.on_conversation_index_updated();
				this.#app_callbacks.on_conversation_updated_main_panel(false);
			} else {
				this.#app_callbacks.close_conversation();
				this.#app_callbacks.on_conversation_updated_main_panel(false);
			}
		}
	}

	//endregion

	//region HTML Generation

	/**
	 * Creates and returns a DOM element for a single scratchpad entry in the index.
	 * @param {Object} conversation - The conversation object.
	 * @param {string} guid - The unique identifier of the scratchpad.
	 * @returns {HTMLElement} The created index item element.
	 */
	create_scratchpad_index_item(conversation, guid) {
		const isChecked = this.#selection.isSelected(guid);
		const title = this.get_scratchpad_title(conversation);
		const isBookmarked = conversation.BOOKMARKED || false;
		const html = this._create_scratchpad_item_html(title, guid, isBookmarked, this.#selection.isSelectionMode, isChecked);
		const index_item = createElementFromHTML(html);
		if (!index_item) return document.createElement('div');

		const chk = index_item.querySelector('.response-item-checkbox');
		if (chk) {
			chk.onclick = (e) => {
				e.stopPropagation();
				this.#selection.toggleItem(guid, chk.checked);
				this._update_selection_ui();
			};
		}

		index_item.onclick = () => {
			if (window.innerWidth <= this.BREAKPOINT_MOBILE) {
				this.#app_callbacks.set_i_open(false);
				this.#app_callbacks.apply_panels_layout();
			}
			this.#app_callbacks.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, guid);
			this.#app_callbacks.on_conversation_updated_main_panel();
		};
		return index_item;
	}

	/**
	 * Generates the HTML string for a single scratchpad index item.
	 * @param {string} title - The title of the scratchpad.
	 * @param {string} guid - The GUID of the scratchpad.
	 * @param {boolean} isBookmarked - Whether it is bookmarked.
	 * @param {boolean} isSelectionMode - Whether selection mode is active.
	 * @param {boolean} isChecked - Whether checked.
	 * @returns {string} The HTML string.
	 */
	_create_scratchpad_item_html(title, guid, isBookmarked, isSelectionMode, isChecked) {
		const iconClass = isBookmarked ? 'bookmark-icon-active' : 'bookmark-icon-inactive';
		return `
			<div class="div-index-item div-index-item-flex ${isBookmarked ? 'div-index-item-bookmarked' : ''}"
				 data-guid="${guid}">
				${isSelectionMode ? `<input type="checkbox" ${isChecked ? 'checked' : ''} class="response-item-checkbox index-item-checkbox-margin">` : ''}
				<span class="index-item-icon as-icon ${iconClass}">&#128278;</span>
				<span class="index-item-text-grow">${title}</span>
			</div>
		`;
	}

	/**
	 * Determines the display title for a scratchpad conversation.
	 * @param {Object} conversation - The conversation object.
	 * @returns {string} The title.
	 */
	get_scratchpad_title(conversation) {
		if (!conversation) return 'New Scratchpad';
		const history = conversation[this.#storage.KEY_CONVERSATION_HISTORY] || [];
		if (history.length > 0) {
			for (let i = history.length - 1; i >= 0; i--) {
				const item = history[i];
				if (item && item.title) {
					return item.title;
				}
			}
		}
		return conversation[this.#storage.KEY_CONVERSATION_TITLE] || 'New Scratchpad';
	}

	/**
	 * Retrieves all scratchpad conversations from the storage index.
	 * @returns {Array<Object>} The scratchpad conversations.
	 */
	get_scratchpad_conversations() {
		const index = this.#storage.get_app_index() || [];
		const scratchpads = [];
		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) {
				scratchpads.push({ guid, conversation: conversation || {}, date_updated: item[this.#storage.KEY_INDEX_DATE_UPDATED] });
			}
		});
		scratchpads.sort((a, b) => (b.date_updated || 0) - (a.date_updated || 0));
		return scratchpads;
	}

	//endregion

	//region UI Updates

	/**
	 * Highlights the active scratchpad conversation in the index.
	 */
	highlight_active_index_item() {
		if (!this.div_index_list) return;
		const config = this.#storage.get_app_config();
		const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];

		const indexItems = this.div_index_list.querySelectorAll('.div-index-item');
		indexItems.forEach((item) => {
			if (item.dataset.guid === selected_guid) {
				item.classList.add('div-index-item-selected');
			} else {
				item.classList.remove('div-index-item-selected');
			}
		});
	}

	/**
	 * Creates and returns the DOM element for the "New Scratchpad" trigger button.
	 * @returns {HTMLElement}
	 */
	create_new_scratchpad_trigger_item() {
		const html = `
			<button class="btn-new-scratchpad">
				<span class="index-item-icon as-icon" style="margin-right: 0.5em;">&#10133;</span>
				<span>New Scratchpad</span>
			</button>
		`;
		const div = createElementFromHTML(html);
		if (!div) return document.createElement('button');

		div.onclick = (e) => {
			e.stopPropagation();
			this.#app_callbacks.create_new_scratchpad();
		};
		return div;
	}

	/**
	 * Renders the scratchpad index panel.
	 */
	on_conversation_index_updated() {
		const scratchpads = this.get_scratchpad_conversations();

		if (!this.div_index_list) return;
		this.div_index_list.innerHTML = '';

		// Add "+ New Scratchpad" creator item at the very top
		this.div_index_list.appendChild(this.create_new_scratchpad_trigger_item());

		if (scratchpads.length > 0) {
			if (this.btn_conversation_index_select) this.btn_conversation_index_select.disabled = false;
		} else {
			if (this.btn_conversation_index_select) this.btn_conversation_index_select.disabled = true;
		}

		this._update_selection_ui();

		if (scratchpads.length > 0) {
			const bookmarked = scratchpads.filter(item => item.conversation.BOOKMARKED === true);
			const others = scratchpads.filter(item => item.conversation.BOOKMARKED !== true);

			bookmarked.forEach(item => {
				this.div_index_list.appendChild(this.create_scratchpad_index_item(item.conversation, item.guid));
			});

			if (bookmarked.length > 0 && others.length > 0) {
				const hr = document.createElement('hr');
				hr.className = 'hr-chat-response-divider';
				this.div_index_list.appendChild(hr);
			}

			others.forEach(item => {
				this.div_index_list.appendChild(this.create_scratchpad_index_item(item.conversation, item.guid));
			});
		}

		this.highlight_active_index_item();
	}

	//endregion
}

export default ScratchpadIndex;
🌐
scratchpad-index.js ×
Type: Web, text/plain
10.19 Kilobytes
Last Modified 2026-09-04 14:24:09
⬇ Download File