1 directories, 28 files

tinai

Home / testing / ai / tinai
/**
 * Class ConversationsList
 * Manages the primary navigation list of conversations.
 * Handles rendering the list, filtering active/archived conversations, 
 * and managing multi-selection for bulk actions.
 */
class ConversationsList {

    #storage;
    #app_callbacks; // { update_app_config, apply_panels_layout, render_conversation_header, close_conversation, set_v_open, set_i_open, on_conversation_updated, set_active_tab }

    div_list;
    div_title;
    btn_select_conversations;
    btn_archive_conversations;
    btn_delete_conversations;
    btn_archives_toggle;
    btn_conversations;
    btn_show_conversations_new;

    state_conversation_select = false;
    state_conversations_selected = [];

    BREAKPOINT_MOBILE;

    constructor(storage_instance, app_callbacks, elements, breakpoints) {
        this.#storage = storage_instance;
        this.#app_callbacks = app_callbacks;

        this.div_list = elements.div_list;
        this.div_title = elements.div_title;
        this.btn_select_conversations = elements.btn_select_conversations;
        this.btn_archive_conversations = elements.btn_archive_conversations;
        this.btn_delete_conversations = elements.btn_delete_conversations;
        this.btn_archives_toggle = elements.btn_archives_toggle;
        this.btn_conversations = elements.btn_conversations;
        this.btn_show_conversations_new = elements.btn_show_conversations_new;

        this.BREAKPOINT_MOBILE = breakpoints.BREAKPOINT_MOBILE;
        
        this.btn_show_conversations_new.onclick = () => this.#app_callbacks.close_conversation();
    }

    //region Conversation Management

    /**
     * Creates a new conversation, updates the index, and sets it as the currently selected conversation.
     */
    index_new(type) {
        const guid = this.#storage.update_app_index('', false, type);
        if (window.innerWidth <= this.BREAKPOINT_MOBILE) {
            this.#app_callbacks.set_v_open(false);
            this.#app_callbacks.set_i_open(false);
        }
        this.#app_callbacks.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, guid);
        if (type === this.#storage.CONVERSATION_TYPE_CHAT) {
            this.#app_callbacks.set_active_tab('conversation');
        } else if (type === this.#storage.CONVERSATION_TYPE_NOTEBOOK) {
            this.#app_callbacks.set_active_tab('document');
        }
        this.#app_callbacks.on_conversation_updated();
    }

    /**
     * Deletes a conversation by its GUID from the application index and localStorage.
     * @param {string} guid The unique identifier of the conversation to delete.
     */
    index_delete(guid) {
        this.#storage.index_delete(guid);
        this.#app_callbacks.apply_panels_layout();
    }

    //endregion

    //region Selection and Actions

    /**
     * Updates the UI elements related to conversation selection (button text and disabled states).
     * @private
     */
    _update_selection_ui() {
        const count = this.state_conversations_selected.length;
        this.btn_select_conversations.textContent = count > 0 ? count : 'Select';
        this.btn_archive_conversations.disabled = count === 0;
        this.btn_delete_conversations.disabled = count === 0;
    }

    /**
     * Toggles the multi-selection mode for managing the list of conversations.
     */
    toggle_conversation_selection_mode() {
        this.state_conversation_select = !this.state_conversation_select;

        if (!this.state_conversation_select) {
            this.state_conversations_selected = [];
        }

        this._update_selection_ui();
        this.on_app_index_updated();
    }

    /**
     * Archives or unarchives all conversations currently selected in the list.
     */
    archive_selected_conversations() {
        const count = this.state_conversations_selected.length;
        if (count === 0) return;

        this.state_conversations_selected.forEach(guid => {
            const conversation = this.#storage.get_conversation(guid);
            const isArchived = conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] || false;
            conversation[this.#storage.KEY_CONVERSATION_ARCHIVED] = !isArchived;
            this.#storage.save_conversation(guid, conversation);
        });

        this.state_conversations_selected = [];
        this.state_conversation_select = false;
        this._update_selection_ui();
        this.on_app_index_updated();
    }

    /**
     * Deletes all conversations currently selected in the list after user confirmation.
     */
    delete_selected_conversations() {
        const count = this.state_conversations_selected.length;
        if (count === 0) return;

        const message = count === 1
            ? "Are you sure you want to delete this conversation?"
            : `Are you sure you want to delete ${count} selected conversations?`;

        if (confirm(message)) {
            const config = this.#storage.get_app_config();
            const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];

            // Copy the array to avoid mutation issues during iteration
            const toDelete = [...this.state_conversations_selected];
            toDelete.forEach(guid => {
                if (selected_guid === guid) {
                    this.#app_callbacks.close_conversation();
                }
                this.index_delete(guid);
            });

            this.state_conversations_selected = [];
            this.state_conversation_select = false;
            this._update_selection_ui();
            this.on_app_index_updated();
        }
    }

    /**
     * Toggles whether archived conversations are displayed in the main conversations list.
     */
    toggle_archives() {
        const config = this.#storage.get_app_config();
        const current = config[this.#storage.KEY_SHOW_ARCHIVES] || false;
        const next = !current;
        this.#app_callbacks.update_app_config(this.#storage.KEY_SHOW_ARCHIVES, next);

        if (!next) {
            const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
            if (selected_guid) {
                const conversation = this.#storage.get_conversation(selected_guid);
                if (conversation[this.#storage.KEY_CONVERSATION_ARCHIVED]) {
                    this.#app_callbacks.close_conversation();
                }
            }
        }

        this.on_app_index_updated();
    }

    //endregion

    //region HTML Generation

    /**
     * Creates and returns a DOM element for a single conversation entry in the navigation list.
     * @param {Object} data The metadata from the application index.
     * @param {string} guid The GUID of the conversation.
     * @param {string} title The display title of the conversation.
     * @param {boolean} is_archived Whether the conversation is currently archived.
     * @param {string} type The type of conversation.
     * @returns {HTMLElement} The created list item element.
     */
    create_index_item_element(data, guid, title, is_archived, type) {
        const isChecked = this.state_conversations_selected.includes(guid);
        const html = this._get_index_item_html(guid, title, is_archived, this.state_conversation_select, isChecked, type);

        const temp = document.createElement('div');
        temp.innerHTML = html.trim();
        const div = temp.firstElementChild;

        div.onclick = () => {
            if (window.innerWidth <= this.BREAKPOINT_MOBILE) {
                this.#app_callbacks.set_v_open(false);
                this.#app_callbacks.set_i_open(false);
            }
            this.#app_callbacks.update_app_config(this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID, guid);
            if (type === this.#storage.CONVERSATION_TYPE_CHAT) {
                this.#app_callbacks.set_active_tab('conversation');
            } else if (type === this.#storage.CONVERSATION_TYPE_NOTEBOOK) {
                this.#app_callbacks.set_active_tab('document');
            }
            this.#app_callbacks.on_conversation_updated();
        };

        const chk = div.querySelector('.index-item-checkbox');
        if (chk) {
            chk.onclick = (e) => {
                e.stopPropagation();
                if (chk.checked) {
                    this.state_conversations_selected.push(guid);
                } else {
                    this.state_conversations_selected = this.state_conversations_selected.filter(id => id !== guid);
                }

                this._update_selection_ui();
            };
        }

        return div;
    }

    /**
     * Generates the HTML string for a single conversation index item.
     * @param {string} guid The GUID of the conversation.
     * @param {string} title The display title of the conversation.
     * @param {boolean} is_archived Whether the conversation is currently archived.
     * @param {boolean} state_conversation_select Whether conversation selection mode is active.
     * @param {boolean} isChecked Whether the checkbox for this item should be checked.
     * @param {string} type The type of conversation.
     * @returns {string} The HTML string for the conversation index item.
     */
    _get_index_item_html(guid, title, is_archived, state_conversation_select, isChecked, type) {
        const icon = type === this.#storage.CONVERSATION_TYPE_NOTEBOOK ? '&#128221;' : '&#128172;';
        return `
            <div class="div-index-item div-index-item-flex ${is_archived ? 'div-conversation-item-archived' : ''}"
                 id="conversation-${guid}">
                ${state_conversation_select ? `<input type="checkbox" ${isChecked ? 'checked' : ''} class="index-item-checkbox index-item-checkbox-margin">` : ''}
                <span class="index-item-icon as-icon">${icon}</span>
                <span class="index-item-text-grow">${title}</span>
            </div>
        `;
    }

    /**
     * Generates the HTML string for the "Archives" header.
     * @returns {string} The HTML string for the archives header.
     */
    _get_archive_header_html() {
        return '<h5 class="div-index-archive-header">Archives</h5>' +
            '<hr/>';
    }

    /**
     * Generates the HTML string for the "No archived conversations" message.
     * @returns {string} The HTML string for the no archived conversations message.
     */
    _get_no_archive_message_html() {
        return '<div class="div-index-archive-empty">No archived conversations</div>';
    }

    //endregion

    //region UI Updates

    /**
     * Renders the entire conversations navigation list, categorized by active and archived status.
     */
    on_app_index_updated(){
        const index = this.#storage.get_app_index();
        this.div_list.innerHTML = '';

        if (index.length === 0) {
            this.#app_callbacks.set_v_open(false);
            this.btn_conversations.disabled = true;
            this.btn_select_conversations.disabled = true;
            this.btn_show_conversations_new.style.display = 'none';
            this.#app_callbacks.apply_panels_layout();
        } else {
            this.btn_conversations.disabled = false;
            this.btn_select_conversations.disabled = false;
            this.btn_show_conversations_new.style.display = '';
        }

        this._update_selection_ui();
        this.btn_archives_toggle.disabled = false;

        const config = this.#storage.get_app_config();
        const showArchives = config[this.#storage.KEY_SHOW_ARCHIVES] || false;

        index.sort((a, b) => b[this.#storage.KEY_INDEX_DATE_UPDATED] - a[this.#storage.KEY_INDEX_DATE_UPDATED]);

        const activeItems = [];
        const archivedItems = [];

        index.forEach(item => {
            const guid = item[this.#storage.KEY_INDEX_GUID];
            const conversation = this.#storage.get_conversation(guid);
            if (conversation[this.#storage.KEY_CONVERSATION_ARCHIVED]) {
                archivedItems.push({ item, conversation });
            } else {
                activeItems.push({ item, conversation });
            }
        });

        activeItems.forEach(data => {
            const guid = data.item[this.#storage.KEY_INDEX_GUID];
            const title = data.conversation[this.#storage.KEY_CONVERSATION_TITLE] || 'New Conversation';
            const type = data.conversation[this.#storage.KEY_CONVERSATION_TYPE] || this.#storage.CONVERSATION_TYPE_CHAT;
            this.div_list.appendChild(this.create_index_item_element(data, guid, title, false, type));
        });

        if (showArchives) {
            const tempHeader = document.createElement('div');
            tempHeader.innerHTML = this._get_archive_header_html().trim();
            while (tempHeader.firstChild) {
                this.div_list.appendChild(tempHeader.firstChild);
            }

            if (archivedItems.length > 0) {
                archivedItems.forEach(data => {
                    const guid = data.item[this.#storage.KEY_INDEX_GUID];
                    const title = data.conversation[this.#storage.KEY_CONVERSATION_TITLE] || 'New Conversation';
                    const type = data.conversation[this.#storage.KEY_CONVERSATION_TYPE] || this.#storage.CONVERSATION_TYPE_CHAT;
                    this.div_list.appendChild(this.create_index_item_element(data, guid, title, true, type));
                });
            } else {
                const tempEmptyMsg = document.createElement('div');
                tempEmptyMsg.innerHTML = this._get_no_archive_message_html().trim();
                this.div_list.appendChild(tempEmptyMsg.firstElementChild);
            }
        }

        const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
        if (!selected_guid) {
            this.div_title.innerHTML = '<h4>Welcome</h4>';
        } else {
            this.#app_callbacks.render_conversation_header();
            this.#app_callbacks.apply_panels_layout()
        }

        this.apply_selected_index_class();
    }

    /**
     * Updates the styling of conversation list items to highlight the currently selected conversation.
     */
    apply_selected_index_class() {
        const config = this.#storage.get_app_config();
        const selected_guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];

        this.div_list.querySelectorAll('.div-index-item.div-index-item-selected').forEach(item => {
            item.classList.remove('div-index-item-selected');
        });

        const selected_item = document.getElementById('conversation-' + selected_guid);
        if (selected_item) {
            selected_item.classList.add('div-index-item-selected');
            this.#app_callbacks.apply_panels_layout()
        }
    }
}

export default ConversationsList;
🌐
conversations.js ×
Type: Web, text/plain
14.49 Kilobytes
Last Modified 2026-04-25 02:55:58
⬇ Download File