1 directories, 28 files

tinai

Home / testing / ai / tinai
/**
 * class ConversationIndex
 * Manages the secondary navigation index for a single conversation.
 * Handles the display, selection, bookmarking, and deletion of individual responses
 * within the active conversation's history.
 */
class ConversationIndex {

    #storage;
    #app_callbacks; // { get_selected_conversation, set_i_open, apply_panels_layout, on_conversation_updated_main_panel }

    div_index_list;
    btn_conversation_index_select;
    btn_conversation_index_favorite;
    btn_conversation_index_delete;

    SCROLL_DELAY;
    BREAKPOINT_MOBILE;

    state_response_select = false;
    state_responses_selected = [];

    constructor(storage_instance, app_callbacks, elements, breakpoints, scroll_delay) {
        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.SCROLL_DELAY = scroll_delay;
        this.BREAKPOINT_MOBILE = breakpoints.BREAKPOINT_MOBILE;
    }

    //region Response Selection and Actions

    /**
     * Toggles multi-selection mode for individual responses within the current conversation.
     */
    toggle_response_selection_mode() {
        this.state_response_select = !this.state_response_select;

        if (!this.state_response_select) {
            this.state_responses_selected = [];
            this.btn_conversation_index_select.textContent = 'Select';
        } else {
            this.btn_conversation_index_select.textContent = this.state_responses_selected.length > 0 ? this.state_responses_selected.length : 'Select';
        }

        this.on_conversation_index_updated();
    }

    /**
     * Toggles the bookmarked status of all responses currently selected in the response index.
     */
    bookmark_selected_responses() {
        const count = this.state_responses_selected.length;
        if (count === 0) return;

        const config = this.#storage.get_app_config();
        const guid = config[this.#storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
        if (!guid) return;

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

        this.state_responses_selected.forEach(index => {
            if (history[index]) {
                const isBookmarked = history[index].BOOKMARKED || false;
                history[index].BOOKMARKED = !isBookmarked;
            }
        });

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

        this.state_responses_selected = [];
        this.state_response_select = false;
        this.btn_conversation_index_select.textContent = 'Select';
        this.on_conversation_index_updated(); // Update the index panel after bookmarking
        this.#app_callbacks.on_conversation_updated_main_panel(false); // Update main panel without scrolling
    }

    /**
     * Deletes all responses currently selected in the response index from the history after user confirmation.
     */
    delete_selected_responses() {
        const count = this.state_responses_selected.length;
        if (count === 0) return;

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

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

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

            // Filter out items whose index is in the selection array
            history = history.filter((_, index) => !this.state_responses_selected.includes(index));

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

            this.state_responses_selected = [];
            this.state_response_select = false;
            this.btn_conversation_index_select.textContent = 'Select';
            this.on_conversation_index_updated(); // Update the index panel after deleting
            this.#app_callbacks.on_conversation_updated_main_panel(false); // Update main panel without scrolling
        }
    }

    //endregion

    //region HTML Generation

    /**
     * Creates and returns a DOM element for a single response entry in the conversation index.
     * @param {Object} data The response data object.
     * @param {number} index The numerical index of the response in history.
     * @returns {HTMLElement} The created index item element.
     */
    create_response_index_item(data, index) {
        const isChecked = this.state_responses_selected.includes(index);
        const html = this._create_response_item_html(data, index, this.state_response_select, isChecked);

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

        const chk = index_item.querySelector('.response-item-checkbox');
        if (chk) {
            chk.onclick = (e) => {
                e.stopPropagation();
                if (chk.checked) {
                    this.state_responses_selected.push(index);
                } else {
                    this.state_responses_selected = this.state_responses_selected.filter(i => i !== index);
                }

                if (this.state_responses_selected.length > 0) {
                    this.btn_conversation_index_select.textContent = this.state_responses_selected.length;
                } else {
                    this.btn_conversation_index_select.textContent = 'Select';
                }
                this.btn_conversation_index_favorite.disabled = this.state_responses_selected.length === 0;
                this.btn_conversation_index_delete.disabled = this.state_responses_selected.length === 0;
            };
        }

        index_item.onclick = () => {
            const el = document.getElementById('chat-item-' + index);
            if (el) {
                if (window.innerWidth <= this.BREAKPOINT_MOBILE) {
                    setTimeout(() => {
                        el.scrollIntoView({ behavior: 'smooth' });
                    }, this.SCROLL_DELAY);
                } else {
                    el.scrollIntoView({ behavior: 'smooth' });
                }
            }
            if (window.innerWidth <= this.BREAKPOINT_MOBILE) {
                this.#app_callbacks.set_i_open(false);
                this.#app_callbacks.apply_panels_layout();
            }
        };
        return index_item;
    }

    /**
     * Generates the HTML string for a single response index item.
     * @param {Object} data The response data object.
     * @param {number} index The numerical index of the response in history.
     * @param {boolean} state_response_select Whether response selection mode is active.
     * @param {boolean} isChecked Whether the checkbox for this item should be checked.
     * @returns {string} The HTML string for the response index item.
     */
    _create_response_item_html(data, index, state_response_select, isChecked) {
        return `
			<div class="div-index-item div-index-item-flex ${data.BOOKMARKED ? 'div-index-item-bookmarked' : ''}"
				 data-index="${index}">
				${state_response_select ? `<input type="checkbox" ${isChecked ? 'checked' : ''} class="response-item-checkbox index-item-checkbox-margin">` : ''}
				<span class="index-item-text-grow">${data.title}</span>
			</div>
		`;
    }
    //endregion

    //region UI Updates

    /**
     * Highlights the item in the response index that corresponds to the chat response currently in the user's viewport.
     */
    highlight_active_index_item() {
        const anchors = document.querySelectorAll('[id^="chat-item-"]');
        const scrollContainer = document.querySelector('.div-chat-container-scroll');
        if (!scrollContainer || anchors.length === 0) return;

        const containerRect = scrollContainer.getBoundingClientRect();
        let activeIndex = 0;

        // Check if the last anchor is visible
        const lastAnchor = anchors[anchors.length - 1];
        const lastRect = lastAnchor.getBoundingClientRect();
        const isLastVisible = (lastRect.top < containerRect.bottom && lastRect.bottom > containerRect.top);

        if (isLastVisible) {
            activeIndex = anchors.length - 1;
        } else {
            // Find anchor closest to the top of the screen/container
            let minDistance = Infinity;
            anchors.forEach((anchor, i) => {
                const rect = anchor.getBoundingClientRect();
                const distance = Math.abs(rect.top - containerRect.top);
                if (distance < minDistance) {
                    minDistance = distance;
                    activeIndex = i;
                }
            });
        }

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

    /**
     * Renders the conversation index panel based on the current conversation history.
     */
    on_conversation_index_updated() {
        const conversation = this.#app_callbacks.get_selected_conversation();
        const history = conversation ? conversation[this.#storage.KEY_CONVERSATION_HISTORY] : null;

        this.div_index_list.innerHTML = '';

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

        if (!history || history.length === 0) {
            this.btn_conversation_index_favorite.disabled = true;
            this.btn_conversation_index_delete.disabled = true;
            return;
        }

        this.btn_conversation_index_favorite.disabled = this.state_responses_selected.length === 0;
        this.btn_conversation_index_delete.disabled = this.state_responses_selected.length === 0;

        const indexedHistory = history.map((data, index) => ({ data, index }));
        const bookmarked = indexedHistory.filter(item => item.data.BOOKMARKED === true);
        const others = indexedHistory.filter(item => item.data.BOOKMARKED !== true);

        bookmarked.forEach(item => {
            if (item.data.title) {
                this.div_index_list.appendChild(this.create_response_index_item(item.data, item.index));
            }
        });

        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 => {
            if (item.data.title) {
                this.div_index_list.appendChild(this.create_response_index_item(item.data, item.index));
            }
        });

        this.highlight_active_index_item();
    }

    //endregion
}

export default ConversationIndex;
🌐
conversation-index.js ×
Type: Web, text/plain
11.46 Kilobytes
Last Modified 2026-04-25 02:55:54
⬇ Download File